diff --git a/.formatter.exs b/.formatter.exs index abd91dbbe..a96afe758 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,3 +1,14 @@ [ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs", "priv/repo/optional_migrations/**/*.exs", "priv/scrubbers/*.ex"] + import_deps: [:ecto, :ecto_sql, :phoenix], + subdirectories: ["priv/*/migrations"], + plugins: [Phoenix.LiveView.HTMLFormatter], + inputs: [ + "mix.exs", + "*.{heex,ex,exs}", + "{config,lib,test}/**/*.{heex,ex,exs}", + "priv/*/seeds.exs", + "priv/repo/migrations/*.exs", + "priv/repo/optional_migrations/**/*.exs", + "priv/scrubbers/*.ex" + ] ] diff --git a/.woodpecker/build-amd64.yml b/.woodpecker/build-amd64.yml index c90d7f394..f7d2ae7d8 100644 --- a/.woodpecker/build-amd64.yml +++ b/.woodpecker/build-amd64.yml @@ -37,7 +37,7 @@ variables: pipeline: # Canonical amd64 debian-bookworm: - image: hexpm/elixir:1.15.4-erlang-25.3.2.5-debian-bookworm-20230612 + image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612 <<: *on-release environment: MIX_ENV: prod @@ -66,7 +66,7 @@ pipeline: - /bin/sh /entrypoint.sh debian-bullseye: - image: hexpm/elixir:1.15.4-erlang-25.3.2.5-debian-bullseye-20230612 + image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bullseye-20230612 <<: *on-release environment: MIX_ENV: prod @@ -94,7 +94,7 @@ pipeline: # Canonical amd64-musl musl: - image: hexpm/elixir:1.14.3-erlang-25.2.2-alpine-3.18.0 + image: hexpm/elixir:1.15.4-erlang-26.0.2-alpine-3.18.2 <<: *on-stable environment: MIX_ENV: prod diff --git a/.woodpecker/build-arm64.yml b/.woodpecker/build-arm64.yml index d923e5847..1ce1fac44 100644 --- a/.woodpecker/build-arm64.yml +++ b/.woodpecker/build-arm64.yml @@ -37,7 +37,7 @@ variables: pipeline: # Canonical arm64 debian-bookworm: - image: hexpm/elixir:1.15.4-erlang-25.3.2.5-debian-bookworm-20230612 + image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612 <<: *on-release environment: MIX_ENV: prod @@ -65,7 +65,7 @@ pipeline: # Canonical arm64-musl musl: - image: hexpm/elixir:1.15.4-erlang-25.3.2.5-alpine-3.18.2 + image: hexpm/elixir:1.15.4-erlang-26.0.2-alpine-3.18.2 <<: *on-stable environment: MIX_ENV: prod diff --git a/.woodpecker/lint.yml b/.woodpecker/lint.yml new file mode 100644 index 000000000..8308e57d7 --- /dev/null +++ b/.woodpecker/lint.yml @@ -0,0 +1,55 @@ +platform: linux/amd64 + +variables: + - &scw-secrets + - SCW_ACCESS_KEY + - SCW_SECRET_KEY + - SCW_DEFAULT_ORGANIZATION_ID + - &setup-hex "mix local.hex --force && mix local.rebar --force" + - &on-release + when: + event: + - push + - tag + branch: + - develop + - stable + - refs/tags/v* + - refs/tags/stable-* + - &on-stable + when: + event: + - push + - tag + branch: + - stable + - refs/tags/stable-* + - &on-point-release + when: + event: + - push + branch: + - develop + - stable + - &on-pr-open + when: + event: + - pull_request + + - &tag-build "export BUILD_TAG=$${CI_COMMIT_TAG:-\"$CI_COMMIT_BRANCH\"} && export PLEROMA_BUILD_BRANCH=$BUILD_TAG" + + - &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)" + - &mix-clean "mix deps.clean --all && mix clean" + +pipeline: + lint: + image: akkoma/ci-base:1.15-otp26 + <<: *on-pr-open + environment: + MIX_ENV: test + commands: + - mix local.hex --force + - mix local.rebar --force + - mix deps.get + - mix compile + - mix format --check-formatted diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml index be8ea0dfa..16a4067fe 100644 --- a/.woodpecker/test.yml +++ b/.woodpecker/test.yml @@ -1,5 +1,8 @@ platform: linux/amd64 +depends_on: + - lint + matrix: ELIXIR_VERSION: - 1.14 @@ -12,9 +15,8 @@ matrix: OTP_VERSION: 25 - ELIXIR_VERSION: 1.15 OTP_VERSION: 25 - # Soon - #- ELIXIR_VERSION: 1.15 - # OTP_VERSION: 26 + - ELIXIR_VERSION: 1.15 + OTP_VERSION: 26 variables: - &scw-secrets @@ -69,15 +71,7 @@ services: POSTGRES_PASSWORD: postgres pipeline: - lint: - <<: *on-pr-open - image: akkoma/ci-base:1.15 - commands: - - mix local.hex --force - - mix local.rebar --force - - mix format --check-formatted - - build: + test: image: akkoma/ci-base:${ELIXIR_VERSION}-otp${OTP_VERSION} <<: *on-pr-open environment: @@ -91,24 +85,9 @@ pipeline: - mix local.rebar --force - mix deps.get - mix compile - - test: - image: akkoma/ci-base:${ELIXIR_VERSION}-otp${OTP_VERSION} - <<: *on-pr-open - environment: - MIX_ENV: test - POSTGRES_DB: pleroma_test_${ELIXIR_VERSION}_${OTP_VERSION} - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - DB_HOST: postgres - commands: - - mix local.hex --force - - mix local.rebar --force - - mix deps.get - - mix compile - - mix ecto.drop -f -q - - mix ecto.create - - mix ecto.migrate - - mkdir -p test/tmp - - mix test --preload-modules --exclude erratic --exclude federated --exclude mocked - - mix test --preload-modules --only mocked + - mix ecto.drop -f -q + - mix ecto.create + - mix ecto.migrate + - mkdir -p test/tmp + - mix test --preload-modules --exclude erratic --exclude federated --exclude mocked + - mix test --preload-modules --only mocked diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b3d1a71..162d25cdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,61 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## Unreleased +## 2024.03 + +## Added +- CLI tasks best-effort checking for past abuse of the recent spoofing exploit +- new `:mrf_steal_emoji, :download_unknown_size` option; defaults to `false` + +## Changed +- `Pleroma.Upload, :base_url` now MUST be configured explicitly if used; + use of the same domain as the instance is **strongly** discouraged +- `:media_proxy, :base_url` now MUST be configured explicitly if used; + use of the same domain as the instance is **strongly** discouraged +- StealEmoji: + - now uses the pack.json format; + existing users must migrate with an out-of-band script (check release notes) + - only steals shortcodes recognised as valid + - URLs of stolen emoji is no longer predictable +- The `Dedupe` upload filter is now always active; + `AnonymizeFilenames` is again opt-in +- received AP data is sanity checked before we attempt to parse it as a user +- Uploads, emoji and media proxy now restrict Content-Type headers to a safe subset +- Akkoma will no longer fetch and parse objects hosted on the same domain ## Fixed +- Critical security issue allowing Akkoma to be used as a vector for + (depending on configuration) impersonation of other users or creation + of bogus users and posts on the upload domain +- Critical security issue letting Akkoma fall for the above impersonation + payloads due to lack of strict id checking +- Critical security issue allowing domains redirect to to pose as the initial domain + (e.g. with media proxy's fallback redirects) +- refetched objects can no longer attribute themselves to third-party actors + (this had no externally visible effect since actor info is read from the Create activity) +- our litepub JSON-LD schema is now served with the correct content type +- remote APNG attachments are now recognised as images + +## 2024.02 + +## Added +- Full compatibility with Erlang OTP26 +- handling of GET /api/v1/preferences +- Akkoma API is now documented +- ability to auto-approve follow requests from users you are already following +- The SimplePolicy MRF can now strip user backgrounds from selected remote hosts + +## Changed +- OTP builds are now built on erlang OTP26 +- The base Phoenix framework is now updated to 1.7 +- An `outbox` field has been added to actor profiles to comply with AP spec +- User profile backgrounds do now federate with other Akkoma instances and Sharkey + +## Fixed +- Documentation issue in which a non-existing nginx file was referenced - Issue where a bad inbox URL could break federation +- Issue where hashtag rel values would be scrubbed +- Issue where short domains listed in `transparency_obfuscate_domains` were not actually obfuscated ## 2023.08 diff --git a/Dockerfile b/Dockerfile index f695322e5..aadd08f7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.15.4-erlang-25.3.2.5-alpine-3.18.2 +FROM hexpm/elixir:1.15.4-erlang-26.0.2-alpine-3.18.2 ENV MIX_ENV=prod ENV ERL_EPMD_ADDRESS=127.0.0.1 diff --git a/SECURITY.md b/SECURITY.md index c009d21d9..d37a8c9ca 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,16 +1,21 @@ -# Pleroma backend security policy - -## Supported versions - -Currently, Pleroma offers bugfixes and security patches only for the latest minor release. - -| Version | Support -|---------| -------- -| 2.2 | Bugfixes and security patches +# Akkoma backend security handling ## Reporting a vulnerability -Please use confidential issues (tick the "This issue is confidential and should only be visible to team members with at least Reporter access." box when submitting) at our [bugtracker](https://git.pleroma.social/pleroma/pleroma/-/issues/new) for reporting vulnerabilities. +Please send an email (preferably encrypted) or +a DM via our IRC to one of the following people: + +| Forgejo nick | IRC nick | Email | GPG | +| ------------ | ------------- | ------------- | --------------------------------------- | +| floatinghost | FloatingGhost | *see GPG key* | https://coffee-and-dreams.uk/pubkey.asc | + ## Announcements -New releases are announced at [pleroma.social](https://pleroma.social/announcements/). All security releases are tagged with ["Security"](https://pleroma.social/announcements/tags/security/). You can be notified of them by subscribing to an Atom feed at . +New releases and security issues are announced at +[meta.akkoma.dev](https://meta.akkoma.dev/c/releases) and +[@akkoma@ihatebeinga.live](https://ihatebeinga.live/akkoma). + +Both also offer RSS feeds +([meta](https://meta.akkoma.dev/c/releases/7.rss), +[fedi](https://ihatebeinga.live/users/akkoma.rss)) +so you can keep an eye on it without any accounts. diff --git a/config/config.exs b/config/config.exs index 3430ee4d7..e0a5eccb1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -61,11 +61,12 @@ # Upload configuration config :pleroma, Pleroma.Upload, uploader: Pleroma.Uploaders.Local, - filters: [Pleroma.Upload.Filter.Dedupe], + filters: [], link_name: false, proxy_remote: false, filename_display_max_length: 30, - base_url: nil + base_url: nil, + allowed_mime_types: ["image", "audio", "video"] config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads" @@ -110,17 +111,6 @@ "xmpp" ] -websocket_config = [ - path: "/websocket", - serializer: [ - {Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"}, - {Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"} - ], - timeout: 60_000, - transport_log: false, - compress: false -] - # Configures the endpoint config :pleroma, Pleroma.Web.Endpoint, url: [host: "localhost"], @@ -130,10 +120,7 @@ {:_, [ {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, - {"/websocket", Phoenix.Endpoint.CowboyWebSocket, - {Phoenix.Transports.WebSocket, - {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}}, - {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}} + {:_, Plug.Cowboy.Handler, {Pleroma.Web.Endpoint, []}} ]} ] ], @@ -162,14 +149,38 @@ format: "$metadata[$level] $message", metadata: [:request_id] +# ——————————————————————————————————————————————————————————————— +# W A R N I N G +# ——————————————————————————————————————————————————————————————— +# +# Whenever adding a privileged new custom type for e.g. +# ActivityPub objects, ALWAYS map their extension back +# to "application/octet-stream". +# Else files served by us can automatically end up with +# those privileged types causing severe security hazards. +# (We need those mappings so Phoenix can assoiate its format +# (the "extension") to incoming requests of those MIME types) +# +# ——————————————————————————————————————————————————————————————— config :mime, :types, %{ "application/xml" => ["xml"], "application/xrd+xml" => ["xrd+xml"], "application/jrd+json" => ["jrd+json"], "application/activity+json" => ["activity+json"], - "application/ld+json" => ["activity+json"] + "application/ld+json" => ["activity+json"], + # Can be removed when bumping MIME past 2.0.5 + # see https://akkoma.dev/AkkomaGang/akkoma/issues/657 + "image/apng" => ["apng"] } +config :mime, :extensions, %{ + "xrd+xml" => "text/plain", + "jrd+json" => "text/plain", + "activity+json" => "text/plain" +} + +# ——————————————————————————————————————————————————————————————— + config :tesla, :adapter, {Tesla.Adapter.Finch, name: MyFinch} # Configures http settings, upstream proxy etc. @@ -300,7 +311,6 @@ alwaysShowSubjectInput: true, background: "/images/city.jpg", collapseMessageWithSubject: false, - disableChat: false, greentext: false, hideFilteredStatuses: false, hideMutedPosts: false, @@ -388,6 +398,7 @@ accept: [], avatar_removal: [], banner_removal: [], + background_removal: [], reject_deletes: [], handle_threads: true @@ -416,8 +427,6 @@ threshold: 604_800, actions: [:delist, :strip_followers] -config :pleroma, :mrf_follow_bot, follower_nickname: nil - config :pleroma, :mrf_reject_newly_created_account_notes, age: 86_400 config :pleroma, :rich_media, @@ -463,10 +472,6 @@ image_quality: 85, min_content_length: 100 * 1024 -config :pleroma, :shout, - enabled: true, - limit: 5_000 - config :phoenix, :format_encoders, json: Jason, "activity+json": Jason config :phoenix, :json_library, Jason diff --git a/config/description.exs b/config/description.exs index e108aaae8..3ddd874f8 100644 --- a/config/description.exs +++ b/config/description.exs @@ -105,6 +105,19 @@ "https://cdn-host.com" ] }, + %{ + key: :allowed_mime_types, + label: "Allowed MIME types", + type: {:list, :string}, + description: + "List of MIME (main) types uploads are allowed to identify themselves with. Other types may still be uploaded, but will identify as a generic binary to clients. WARNING: Loosening this over the defaults can lead to security issues. Removing types is safe, but only add to the list if you are sure you know what you are doing.", + suggestions: [ + "image", + "audio", + "video", + "font" + ] + }, %{ key: :proxy_remote, type: :boolean, diff --git a/docker-compose.yml b/docker-compose.yml index 0dedbc87e..58abf189d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,7 @@ services: db: image: akkoma-db:latest build: ./docker-resources/database + shm_size: 4gb restart: unless-stopped user: ${DOCKER_USER} environment: { diff --git a/docs/docs/administration/CLI_tasks/robots_txt.md b/docs/docs/administration/CLI_tasks/robots_txt.md index 924f2e319..de27e5dca 100644 --- a/docs/docs/administration/CLI_tasks/robots_txt.md +++ b/docs/docs/administration/CLI_tasks/robots_txt.md @@ -17,5 +17,5 @@ If you want to generate a restrictive `robots.txt`, you can run the following mi === "From Source" ```sh - mix pleroma.robotstxt disallow_all + mix pleroma.robots_txt disallow_all ``` diff --git a/docs/docs/administration/CLI_tasks/security.md b/docs/docs/administration/CLI_tasks/security.md new file mode 100644 index 000000000..a0208c4e5 --- /dev/null +++ b/docs/docs/administration/CLI_tasks/security.md @@ -0,0 +1,56 @@ +# Security-related tasks + +{! administration/CLI_tasks/general_cli_task_info.include !} + +!!! danger + Many of these tasks were written in response to a patched exploit. + It is recommended to run those very soon after installing its respective security update. + Over time with db migrations they might become less accurate or be removed altogether. + If you never ran an affected version, there’s no point in running them. + +## Spoofed AcitivityPub objects exploit (2024-03, fixed in 3.11.1) + +### Search for uploaded spoofing payloads + +Scans local uploads for spoofing payloads. +If the instance is not using the local uploader it was not affected. +Attachments wil be scanned anyway in case local uploader was used in the past. + +!!! note + This cannot reliably detect payloads attached to deleted posts. + +=== "OTP" + + ```sh + ./bin/pleroma_ctl security spoof-uploaded + ``` + +=== "From Source" + + ```sh + mix pleroma.security spoof-uploaded + ``` + +### Search for counterfeit posts in database + +Scans all notes in the database for signs of being spoofed. + +!!! note + Spoofs targeting local accounts can be detected rather reliably + (with some restrictions documented in the task’s logs). + Counterfeit posts from remote users cannot. A best-effort attempt is made, but + a thorough attacker can avoid this and it may yield a small amount of false positives. + + Should you find counterfeit posts of local users, let other admins know so they can delete the too. + +=== "OTP" + + ```sh + ./bin/pleroma_ctl security spoof-inserted + ``` + +=== "From Source" + + ```sh + mix pleroma.security spoof-inserted + ``` diff --git a/docs/docs/administration/backup.md b/docs/docs/administration/backup.md index 5c5df88ce..6aa79043c 100644 --- a/docs/docs/administration/backup.md +++ b/docs/docs/administration/backup.md @@ -45,3 +45,16 @@ 8. Remove the dependencies that you don't need anymore (see installation guide). Make sure you don't remove packages that are still needed for other software that you have running! [¹]: We assume the database name and user are both "akkoma". If not, you can find the correct name in your config files. + +## Docker installations + +If running behind Docker, it is required to run the above commands inside of a running database container. + +### Example +Running `docker compose run --rm db pg_dump <...>` will fail and return: +``` +pg_dump: error: connection to server on socket "/run/postgresql/.s.PGSQL.5432" failed: No such file or directory +Is the server running locally and accepting connections on that socket?" +``` +However, first starting just the database container with `docker compose up db -d`, and then running `docker compose exec db pg_dump -d akkoma --format=custom -f ` will successfully generate a database dump. +Then to make the file accessible on the host system you can run `docker compose cp db: ` to copy if from the container. diff --git a/docs/docs/administration/monitoring.md b/docs/docs/administration/monitoring.md index 9233fbe34..b7a748731 100644 --- a/docs/docs/administration/monitoring.md +++ b/docs/docs/administration/monitoring.md @@ -3,7 +3,7 @@ If you run akkoma, you may be inclined to collect metrics to ensure your instance is running smoothly, and that there's nothing quietly failing in the background. -To facilitate this, akkoma exposes prometheus metrics to be scraped. +To facilitate this, akkoma exposes a dashboard and prometheus metrics to be scraped. ## Prometheus @@ -31,3 +31,15 @@ Once you have your token of the form `Bearer $ACCESS_TOKEN`, you can use that in - targets: - example.com ``` + +## Dashboard + +Administrators can access a live dashboard under `/phoenix/live_dashboard` +giving an overview of uptime, software versions, database stats and more. + +The dashboard also includes a variation of the prometheus metrics, however +they do not exactly match due to respective limitations of the dashboard +and the prometheus exporter. +Even more important, the dashboard collects metrics locally in the browser +only while the page is open and cannot give a view on their past history. +For proper monitoring it is recommended to set up prometheus. diff --git a/docs/docs/configuration/cheatsheet.md b/docs/docs/configuration/cheatsheet.md index 73fdf9eea..c4259c6cf 100644 --- a/docs/docs/configuration/cheatsheet.md +++ b/docs/docs/configuration/cheatsheet.md @@ -104,31 +104,60 @@ To add configuration to your config file, you can copy it from the base config. ## Message rewrite facility ### :mrf -* `policies`: Message Rewrite Policy, either one or a list. Here are the ones available by default: - * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default). - * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production. - * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)). - * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive). - * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)). - * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)). - * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:. - * `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links. - * `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed. - * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). - * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). - * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). - * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections. - * `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines. - * `Pleroma.Web.ActivityPub.MRF.FollowBotPolicy`: Automatically follows newly discovered users from the specified bot account. Local accounts, locked accounts, and users with "#nobot" in their bio are respected and excluded from being followed. - * `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot. - * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)). - * `Pleroma.Web.ActivityPub.MRF.NormalizeMarkup`: Pass inbound HTML through a scrubber to make sure it doesn't have anything unusual in it. On by default, cannot be turned off. - * `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Append a link to a post that quotes another post with the link to the quoted post, to ensure that software that does not understand quotes can have full context. On by default, cannot be turned off. * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. * `transparency_obfuscate_domains`: Show domains with `*` in the middle, to censor them if needed. For example, `ridingho.me` will show as `rid*****.me` +* `policies`: Message Rewrite Policy, either one or a list. Here are the ones available by default: + * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default). + * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production. + * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections. + (See [`:mrf_activity_expiration`](#mrf_activity_expiration)) + * `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot. + * `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links. + * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:. + * `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines. + * `Pleroma.Web.ActivityPub.MRF.HellthreadPolicy`: Blocks messages with too many mentions. + (See [`mrf_hellthread`](#mrf_hellthread)) + * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)). + * `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed. + * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). + * `Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy`: Drops local activities which have no actual content. + (e.g. no attachments and only consists of mentions) + * `Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy`: Strips content placeholders from posts + (such as the dot from mastodon) + * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). + * `Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy`: Rejects posts of users the server only recently learned about for a while. Great to block spam accounts. (See [`:mrf_reject_newly_created_account_notes`](#mrf_reject_newly_created_account_notes)) + * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)). + * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)). + * `Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy`: Steals all eligible emoji encountered in posts from remote instances + (See [`:mrf_steal_emoji`](#mrf_steal_emoji)) + * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)). + * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive). + * `Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy`: Drops all posts except from users specified in a list. + (See [`:mrf_user_allowlist`](#mrf_user_allowlist)) + * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). + +Additionally the following MRFs will *always* be aplied and cannot be disabled: + +* `Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy`: Strips users limiting who can send them DMs from the recipients of non-eligible DMs +* `Pleroma.Web.ActivityPub.MRF.HashtagPolicy`: Depending on a post’s hashtags it can be rejected, get its sensitive flags force-enabled or removed from the global timeline + (See [`:mrf_hashtag`](#mrf_hashtag)) +* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Append a link to a post that quotes another post with the link to the quoted post, to ensure that software that does not understand quotes can have full context. + (See [`:mrf_inline_quote`](#mrf_inline_quote)) +* `Pleroma.Web.ActivityPub.MRF.NormalizeMarkup`: Pass inbound HTML through a scrubber to make sure it doesn't have anything unusual in it. + (See [`:mrf_normalize_markup`](#mrf_normalize_markup)) + ## Federation +### :activitypub +* `unfollow_blocked`: Whether blocks result in people getting unfollowed +* `outgoing_blocks`: Whether to federate blocks to other instances +* `blockers_visible`: Whether a user can see the posts of users who blocked them +* `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question +* `sign_object_fetches`: Sign object fetches with HTTP signatures +* `authorized_fetch_mode`: Require HTTP signatures for AP fetches +* `max_collection_objects`: The maximum number of objects to fetch from a remote AP collection. + ### MRF policies !!! note @@ -144,6 +173,7 @@ To add configuration to your config file, you can copy it from the base config. * `report_removal`: List of instances to reject reports from and the reason for doing so. * `avatar_removal`: List of instances to strip avatars from and the reason for doing so. * `banner_removal`: List of instances to strip banners from and the reason for doing so. +* `background_removal`: List of instances to strip user backgrounds from and the reason for doing so. * `reject_deletes`: List of instances to reject deletions from and the reason for doing so. #### :mrf_subchain @@ -206,7 +236,9 @@ config :pleroma, :mrf_user_allowlist, %{ #### :mrf_steal_emoji * `hosts`: List of hosts to steal emojis from * `rejected_shortcodes`: Regex-list of shortcodes to reject -* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk +* `size_limit`: File size limit (in bytes), checked before download if possible (and remote server honest), + otherwise or again checked before saving emoji to the disk +* `download_unknown_size`: whether to download an emoji when the remote server doesn’t report its size in advance #### :mrf_activity_expiration @@ -222,14 +254,24 @@ Notes: - The hashtags in the configuration do not have a leading `#`. - This MRF Policy is always enabled, if you want to disable it you have to set empty lists -### :activitypub -* `unfollow_blocked`: Whether blocks result in people getting unfollowed -* `outgoing_blocks`: Whether to federate blocks to other instances -* `blockers_visible`: Whether a user can see the posts of users who blocked them -* `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question -* `sign_object_fetches`: Sign object fetches with HTTP signatures -* `authorized_fetch_mode`: Require HTTP signatures for AP fetches -* `max_collection_objects`: The maximum number of objects to fetch from a remote AP collection. +#### :mrf_reject_newly_created_account_notes +After initially encountering an user, all their posts +will be rejected for the configured time (in seconds). +Only drops posts. Follows, reposts, etc. are not affected. + +* `age`: Time below which to reject (in seconds) + +An example: (86400 seconds = 24 hours) + +```elixir +config :pleroma, :mrf_reject_newly_created_account_notes, age: 86400 +``` + +#### :mrf_inline_quote +* `prefix`: what prefix to prepend to quoted URLs + +#### :mrf_normalize_markup +* `scrub_policy`: the scrubbing module to use (by default a built-in HTML sanitiser) ## Pleroma.User @@ -356,7 +398,8 @@ This section describe PWA manifest instance-specific values. Currently this opti ## :media_proxy * `enabled`: Enables proxying of remote media to the instance’s proxy -* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts. +* `base_url`: The base URL to access a user-uploaded file. + Using a (sub)domain distinct from the instance endpoint is **strongly** recommended. * `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`. * `whitelist`: List of hosts with scheme to bypass the mediaproxy (e.g. `https://example.com`) * `invalidation`: options for remove media from cache after delete object: @@ -557,8 +600,9 @@ the source code is here: [kocaptcha](https://github.com/koto-bank/kocaptcha). Th * `uploader`: Which one of the [uploaders](#uploaders) to use. * `filters`: List of [upload filters](#upload-filters) to use. -* `link_name`: When enabled Akkoma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe` -* `base_url`: The base URL to access a user-uploaded file. Useful when you want to host the media files via another domain or are using a 3rd party S3 provider. +* `link_name`: When enabled Akkoma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers +* `base_url`: The base URL to access a user-uploaded file; MUST be configured explicitly. + Using a (sub)domain distinct from the instance endpoint is **strongly** recommended. * `proxy_remote`: If you're using a remote uploader, Akkoma will proxy media requests instead of redirecting to it. * `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation. * `filename_display_max_length`: Set max length of a filename to display. 0 = no limit. Default: 30. @@ -598,17 +642,18 @@ config :ex_aws, :s3, ### Upload filters -#### Pleroma.Upload.Filter.AnonymizeFilename - -This filter replaces the filename (not the path) of an upload. For complete obfuscation, add -`Pleroma.Upload.Filter.Dedupe` before AnonymizeFilename. - -* `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used. You can get the original filename extension by using `{extension}`, for example `custom-file-name.{extension}`. - #### Pleroma.Upload.Filter.Dedupe +**Always** active; cannot be turned off. +Renames files to their hash and prevents duplicate files filling up the disk. No specific configuration. +#### Pleroma.Upload.Filter.AnonymizeFilename + +This filter replaces the declared filename (not the path) of an upload. + +* `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used. You can get the original filename extension by using `{extension}`, for example `custom-file-name.{extension}`. + #### Pleroma.Upload.Filter.Exiftool This filter only strips the GPS and location metadata with Exiftool leaving color profiles and attributes intact. @@ -958,6 +1003,15 @@ config :ueberauth, Ueberauth, ] ``` +You may also need to set up your frontend to use oauth logins. For example, for `akkoma-fe`: + +```elixir +config :pleroma, :frontend_configurations, + pleroma_fe: %{ + loginMethod: "token" + } +``` + ## Link parsing ### :uri_schemes diff --git a/docs/docs/configuration/hardening.md b/docs/docs/configuration/hardening.md index 521183f7d..f8ba048dd 100644 --- a/docs/docs/configuration/hardening.md +++ b/docs/docs/configuration/hardening.md @@ -17,6 +17,16 @@ This sets the Akkoma application server to only listen to the localhost interfac This sets the `secure` flag on Akkoma’s session cookie. This makes sure, that the cookie is only accepted over encrypted HTTPs connections. This implicitly renames the cookie from `pleroma_key` to `__Host-pleroma-key` which enforces some restrictions. (see [cookie prefixes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Cookie_prefixes)) +### `Pleroma.Upload, :uploader, :base_url` + +> Recommended value: *anything on a different domain than the instance endpoint; e.g. https://media.myinstance.net/* + +Uploads are user controlled and (unless you’re running a true single-user +instance) should therefore not be considered trusted. But the domain is used +as a pivilege boundary e.g. by HTTP content security policy and ActivityPub. +Having uploads on the same domain enabled several past vulnerabilities +able to be exploited by malicious users. + ### `:http_security` > Recommended value: `true` diff --git a/docs/docs/configuration/howto_mediaproxy.md b/docs/docs/configuration/howto_mediaproxy.md index 8ad81bdfb..223ad7eed 100644 --- a/docs/docs/configuration/howto_mediaproxy.md +++ b/docs/docs/configuration/howto_mediaproxy.md @@ -6,7 +6,16 @@ With the `mediaproxy` function you can use nginx to cache this content, so users ## Activate it -* Edit your nginx config and add the following location: +* Edit your nginx config and add the following location to your main server block: +``` +location /proxy { + return 404; +} +``` + +* Set up a subdomain for the proxy with its nginx config on the same machine + *(the latter is not strictly required, but for simplicity we’ll assume so)* +* In this subdomain’s server block add ``` location /proxy { proxy_cache akkoma_media_cache; @@ -26,9 +35,9 @@ config :pleroma, :media_proxy, enabled: true, proxy_opts: [ redirect_on_failure: true - ] - #base_url: "https://cache.akkoma.social" + ], + base_url: "https://cache.akkoma.social" ``` -If you want to use a subdomain to serve the files, uncomment `base_url`, change the url and add a comma after `true` in the previous line. +You **really** should use a subdomain to serve proxied files; while we will fix bugs resulting from this, serving arbitrary remote content on your main domain namespace is a significant attack surface. * Restart nginx and Akkoma diff --git a/docs/docs/configuration/howto_theming_your_instance.md b/docs/docs/configuration/howto_theming_your_instance.md index 093c12763..e29143db8 100644 --- a/docs/docs/configuration/howto_theming_your_instance.md +++ b/docs/docs/configuration/howto_theming_your_instance.md @@ -60,7 +60,7 @@ Example of `my-awesome-theme.json` where we add the name "My Awesome Theme" ### Set as default theme -Now we can set the new theme as default in the [Pleroma FE configuration](https://docs-fe.akkoma.dev/stable/CONFIGURATION). +Now we can set the new theme as default in the [Pleroma FE configuration](https://docs-fe.akkoma.dev/stable/CONFIGURATION/). Example of adding the new theme in the back-end config files ```elixir diff --git a/docs/docs/configuration/mrf.md b/docs/docs/configuration/mrf.md index a74cfa90d..0a17b3112 100644 --- a/docs/docs/configuration/mrf.md +++ b/docs/docs/configuration/mrf.md @@ -35,6 +35,7 @@ Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_si * `media_removal`: Servers in this group will have media stripped from incoming messages. * `avatar_removal`: Avatars from these servers will be stripped from incoming messages. * `banner_removal`: Banner images from these servers will be stripped from incoming messages. +* `background_removal`: User background images from these servers will be stripped from incoming messages. * `report_removal`: Servers in this group will have their reports (flags) rejected. * `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields. * `reject_deletes`: Deletion requests will be rejected from these servers. @@ -61,6 +62,32 @@ config :pleroma, :mrf_simple, The effects of MRF policies can be very drastic. It is important to use this functionality carefully. Always try to talk to an admin before writing an MRF policy concerning their instance. +## Hiding or Obfuscating Policies + +You can opt out of publicly displaying all MRF policies or only hide or obfuscate selected domains. + +To just hide everything set: + +```elixir +config :pleroma, :mrf, + ... + transparency: false, +``` + +To hide or obfuscate only select entries, use: + +```elixir +config :pleroma, :mrf, + ... + transparency_obfuscate_domains: ["handholdi.ng", "badword.com"], + transparency_exclusions: [{"ghost.club", "even a fragment is too spoopy for humans"}] +``` + +## More MRF Policies + +See the [documentation cheatsheet](cheatsheet.md) +for all available MRF policies and their options. + ## Writing your own MRF Policy As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `policies` config setting. diff --git a/docs/docs/configuration/optimisation/optimizing_beam.md b/docs/docs/configuration/optimisation/optimizing_beam.md index bbdc49725..747773f3e 100644 --- a/docs/docs/configuration/optimisation/optimizing_beam.md +++ b/docs/docs/configuration/optimisation/optimizing_beam.md @@ -25,11 +25,14 @@ Tuning the BEAM requires you provide a config file normally called [vm.args](htt `ExecStart=/usr/bin/elixir --erl '-args_file /opt/akkoma/config/vm.args' -S /usr/bin/mix phx.server` +If using an OTP release, set the `RELEASE_VM_ARGS` environment variable to the path to the vm.args file. + Check your OS documentation to adopt a similar strategy on other platforms. ### Virtual Machine and/or few CPU cores -Disable the busy-waiting. This should generally only be done if you're on a platform that does burst scheduling, like AWS. +Disable the busy-waiting. This should generally be done if you're on a platform that does burst scheduling, like AWS, or if you're running other +services on the same machine. **vm.args:** @@ -39,6 +42,8 @@ Disable the busy-waiting. This should generally only be done if you're on a plat +sbwtdio none ``` +These settings are enabled by default for OTP releases + ### Dedicated Hardware Enable more busy waiting, increase the internal maximum limit of BEAM processes and ports. You can use this if you run on dedicated hardware, but it is not necessary. diff --git a/docs/docs/development/API/akkoma_api.md b/docs/docs/development/API/akkoma_api.md new file mode 100644 index 000000000..cc3f7ddec --- /dev/null +++ b/docs/docs/development/API/akkoma_api.md @@ -0,0 +1,146 @@ +# Akkoma API + +Request authentication (if required) and parameters work the same as for [Pleroma API](pleroma_api.md). + +## `/api/v1/akkoma/preferred_frontend/available` +### Returns the available frontends which can be picked as the preferred choice +* Method: `GET` +* Authentication: not required +* Params: none +* Response: JSON +* Example response: +```json +["pleroma-fe/stable"] +``` + +!!! note + There’s also a browser UI under `/akkoma/frontend` + for interactively querying and changing this. + +## `/api/v1/akkoma/preferred_frontend` +### Configures the preferred frontend of this session +* Method: `PUT` +* Authentication: not required +* Params: + * `frontend_name`: STRING containing one of the available frontends +* Response: JSON +* Example response: +```json +{"frontend_name":"pleroma-fe/stable"} +``` + +!!! note + There’s also a browser UI under `/akkoma/frontend` + for interactively querying and changing this. + +## `/api/v1/akkoma/metrics` +### Provides metrics for Prometheus to scrape +* Method: `GET` +* Authentication: required (admin:metrics) +* Params: none +* Response: text +* Example response: +``` +# HELP pleroma_remote_users_total +# TYPE pleroma_remote_users_total gauge +pleroma_remote_users_total 25 +# HELP pleroma_local_statuses_total +# TYPE pleroma_local_statuses_total gauge +pleroma_local_statuses_total 17 +# HELP pleroma_domains_total +# TYPE pleroma_domains_total gauge +pleroma_domains_total 4 +# HELP pleroma_local_users_total +# TYPE pleroma_local_users_total gauge +pleroma_local_users_total 3 +... +``` + +## `/api/v1/akkoma/translation/languages` +### Returns available source and target languages for automated text translation +* Method: `GET` +* Authentication: required +* Params: none +* Response: JSON +* Example response: +```json +{ + "source": [ + {"code":"LV", "name":"Latvian"}, + {"code":"ZH", "name":"Chinese (traditional)"}, + {"code":"EN-US", "name":"English (American)"} + ], + "target": [ + {"code":"EN-GB", "name":"English (British)"}, + {"code":"JP", "name":"Japanese"} + ] +} +``` + +## `/api/v1/akkoma/frontend_settings/:frontend_name` +### Lists all configuration profiles of the selected frontend for the current user +* Method: `GET` +* Authentication: required +* Params: none +* Response: JSON +* Example response: +```json +[ + {"name":"default","version":31} +] +``` + +## `/api/v1/akkoma/frontend_settings/:frontend_name/:profile_name` +### Returns the full selected frontend settings profile of the current user +* Method: `GET` +* Authentication: required +* Params: none +* Response: JSON +* Example response: +```json +{ + "version": 31, + "settings": { + "streaming": true, + "conversationDisplay": "tree", + ... + } +} +``` + +## `/api/v1/akkoma/frontend_settings/:frontend_name/:profile_name` +### Updates the frontend settings profile +* Method: `PUT` +* Authentication: required +* Params: + * `version`: INTEGER + * `settings`: JSON object containing the entire new settings +* Response: JSON +* Example response: +```json +{ + "streaming": false, + "conversationDisplay": "tree", + ... +} +``` + +!!! note + The `version` field must be increased by exactly one on each update + +## `/api/v1/akkoma/frontend_settings/:frontend_name/:profile_name` +### Drops the specified frontend settings profile +* Method: `DELETE` +* Authentication: required +* Params: none +* Response: JSON +* Example response: +```json +{"deleted":"ok"} +``` + + +## `/api/v1/timelines/bubble` +### Returns a timeline for the local and closely related instances +Works like all other Mastodon-API timeline queries with the documented +[Akkoma-specific additions and tweaks](./differences_in_mastoapi_responses.md#timelines). diff --git a/docs/docs/development/API/differences_in_mastoapi_responses.md b/docs/docs/development/API/differences_in_mastoapi_responses.md index 053dc9663..990806cea 100644 --- a/docs/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/docs/development/API/differences_in_mastoapi_responses.md @@ -1,6 +1,6 @@ # Differences in Mastodon API responses from vanilla Mastodon -A Akkoma instance can be identified by " (compatible; Pleroma )" present in `version` field in response from `/api/v1/instance` +A Akkoma instance can be identified by " (compatible; Akkoma )" present in `version` field in response from `/api/v1/instance` ## Flake IDs @@ -8,20 +8,28 @@ Akkoma uses 128-bit ids as opposed to Mastodon's 64 bits. However, just like Mas ## Timelines +In addition to Mastodon’s timelines, there is also a “bubble timeline” showing +posts from the local instance and a set of closely related instances as chosen +by the administrator. It is available under `/api/v1/timelines/bubble`. + Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users. Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`. -Adding the parameter `reply_visibility` to the public and home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you. +Adding the parameter `reply_visibility` to the public, bubble or home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you. Adding the parameter `instance=lain.com` to the public timeline will show only statuses originating from `lain.com` (or any remote instance). -Home, public, hashtag & list timelines accept these parameters: +All but the direct timeline accept these parameters: - `only_media`: show only statuses with media attached -- `local`: show only local statuses - `remote`: show only remote statuses +Home, public, hashtag & list timelines further accept: + +- `local`: show only local statuses + + ## Statuses - `visibility`: has additional possible values `list` and `local` (for local-only statuses) @@ -113,6 +121,12 @@ Has these additional fields under the `pleroma` object: - `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned. - `favicon`: nullable URL string, Favicon image of the user's instance +Has these additional fields under the `akkoma` object: + +- `instance`: nullable object with metadata about the user’s instance +- `status_ttl_days`: nullable int, default time after which statuses are deleted +- `permit_followback`: boolean, whether follows from followed accounts are auto-approved + ### Source Has these additional fields under the `pleroma` object: diff --git a/docs/docs/installation/generic_dependencies.include b/docs/docs/installation/generic_dependencies.include index d8cf9f9da..b23736d85 100644 --- a/docs/docs/installation/generic_dependencies.include +++ b/docs/docs/installation/generic_dependencies.include @@ -2,7 +2,7 @@ * PostgreSQL 9.6+ * Elixir 1.14+ -* Erlang OTP 24+ +* Erlang OTP 25+ * git * file / libmagic * gcc (clang might also work) diff --git a/docs/docs/installation/migrating_to_akkoma.md b/docs/docs/installation/migrating_to_akkoma.md index 4a58e836e..bf6eeebc7 100644 --- a/docs/docs/installation/migrating_to_akkoma.md +++ b/docs/docs/installation/migrating_to_akkoma.md @@ -21,6 +21,33 @@ fork of Akkoma - luckily this isn't very hard. You'll need to update the backend, then possibly the frontend, depending on your setup. +## Backup diverging features + +As time goes on Akkoma and Pleroma added or removed different features +and reorganised the database in a different way. If you want to be able to +migrate back to Pleroma without losing any affected data, you’ll want to +make a backup before starting the migration. +If you're not interested in migrating back, skip this section +*(although it might be a good idea to temporarily keep a full DB backup +just in case something unexpected happens during migration)* + +As of 2024-02 you will want to keep a backup of: + +- the entire `chats` and `chat_message_references` tables + +The following columns are not deleted by a migration to Akkoma, but a migration +back to Pleroma or future Akkoma upgrades might affect them, so perhaps back them up as well: + +- the `birthday` of users and their `show_birthday` setting +- the `expires_at` key of in the `user_relationships` table + *(used by temporary mutes)* + +The way cached instance metadata is stored differs, but since those +will be refetched and updated anyway, there’s no need for a backup. + +Best check all newer migrations unique to Akkoma/Pleroma +to get an up-to-date picture of what needs to be kept. + ## From Source If you're running the source Akkoma install, you'll need to set the @@ -34,16 +61,7 @@ git pull -r # to run "git merge stable" instead (or develop if you want) ``` -### WARNING - Migrating from Pleroma Develop -If you are on pleroma develop, and have updated since 2022-08, you may have issues with database migrations. - -Please roll back the given migrations: - -```bash -MIX_ENV=prod mix ecto.rollback --migrations-path priv/repo/optional_migrations/pleroma_develop_rollbacks -n3 -``` - -Then compile, migrate and restart as usual. +And compile as usual. ## From OTP @@ -53,15 +71,44 @@ This will just be setting the update URL - find your flavour from the [mapping o export FLAVOUR=[the flavour you found above] ./bin/pleroma_ctl update --zip-url https://akkoma-updates.s3-website.fr-par.scw.cloud/stable/akkoma-$FLAVOUR.zip -./bin/pleroma_ctl migrate ``` -Then restart. When updating in the future, you canjust use +When updating in the future, you can just use ```bash ./bin/pleroma_ctl update --branch stable ``` + +## Database Migrations +### WARNING - Migrating from Pleroma past 2022-08 +If you are on Pleroma stable >= 2.5.0 or Pleroma develop, and +have updated since 2022-08, you may have issues with database migrations. + +Please first roll back the given migrations: + +=== "OTP" + ```bash + ./bin/pleroma_ctl rollback --migrations-path priv/repo/optional_migrations/pleroma_develop_rollbacks -n5 + ``` +=== "From Source" + ```bash + MIX_ENV=prod mix ecto.rollback --migrations-path priv/repo/optional_migrations/pleroma_develop_rollbacks -n5 + ``` + +### Applying Akkoma Database Migrations + +Just run + +=== "OTP" + ```bash + ./bin/pleroma_ctl migrate + ``` +=== "From Source" + ```bash + MIX_ENV=prod mix ecto.migrate + ``` + ## Frontend changes Akkoma comes with a few frontend changes as well as backend ones, @@ -130,3 +177,4 @@ MIX_ENV=prod mix ecto.rollback --to 20210416051708 ``` Then switch back to Pleroma for updates (similar to how was done to migrate to Akkoma), and remove the front-ends. The front-ends are installed in the `frontends` folder in the [static directory](../configuration/static_dir.md). Once you are back to Pleroma, you will need to run the database migrations again. See the Pleroma documentation for this. +After this use your previous backups to restore data from diverging features. diff --git a/docs/docs/installation/otp_en.md b/docs/docs/installation/otp_en.md index b613e17b2..8a8ae077b 100644 --- a/docs/docs/installation/otp_en.md +++ b/docs/docs/installation/otp_en.md @@ -5,7 +5,7 @@ This guide covers a installation using an OTP release. To install Akkoma from source, please check out the corresponding guide for your distro. ## Pre-requisites -* A machine running Linux with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and an `x86_64` CPU you have root access to. If you are not sure if it's compatible see [Detecting flavour section](#detecting-flavour) below +* A machine running Linux with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and an `x86_64` or `arm64` CPU you have root access to. If you are not sure if it's compatible see [Detecting flavour section](#detecting-flavour) below * For installing OTP releases on RedHat-based distros like Fedora and Centos Stream, please follow [this guide](./otp_redhat_en.md) instead. * A (sub)domain pointed to the machine @@ -187,18 +187,18 @@ The location of nginx configs is dependent on the distro === "Alpine" ``` - cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.conf + cp /opt/akkoma/installation/akkoma.nginx /etc/nginx/conf.d/akkoma.conf ``` === "Debian/Ubuntu" ``` - cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/sites-available/akkoma.conf + cp /opt/akkoma/installation/akkoma.nginx /etc/nginx/sites-available/akkoma.conf ln -s /etc/nginx/sites-available/akkoma.conf /etc/nginx/sites-enabled/akkoma.conf ``` If your distro does not have either of those you can append `include /etc/nginx/akkoma.conf` to the end of the http section in /etc/nginx/nginx.conf and ```sh -cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/akkoma.conf +cp /opt/akkoma/installation/akkoma.nginx /etc/nginx/akkoma.conf ``` #### Edit the nginx config diff --git a/docs/docs/installation/otp_redhat_en.md b/docs/docs/installation/otp_redhat_en.md index 1490d3139..ea27af6f4 100644 --- a/docs/docs/installation/otp_redhat_en.md +++ b/docs/docs/installation/otp_redhat_en.md @@ -178,7 +178,7 @@ certbot certonly --standalone --preferred-challenges http -d yourinstance.tld #### Copy Akkoma nginx configuration to the nginx folder ```shell -cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.conf +cp /opt/akkoma/installation/akkoma.nginx /etc/nginx/conf.d/akkoma.conf ``` #### Edit the nginx config diff --git a/installation/nginx/akkoma.nginx b/installation/nginx/akkoma.nginx index 18d92f30f..1d91ce22f 100644 --- a/installation/nginx/akkoma.nginx +++ b/installation/nginx/akkoma.nginx @@ -75,9 +75,48 @@ server { proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + location ~ ^/(media|proxy) { + return 404; + } + location / { proxy_pass http://phoenix; } +} + +# Upload and MediaProxy Subdomain +# (see main domain setup for more details) +server { + server_name media.example.tld; + + listen 80; + listen [::]:80; + + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + server_name media.example.tld; + + listen 443 ssl http2; + listen [::]:443 ssl http2; + + ssl_trusted_certificate /etc/letsencrypt/live/media.example.tld/chain.pem; + ssl_certificate /etc/letsencrypt/live/media.example.tld/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/media.example.tld/privkey.pem; + # .. copy all other the ssl_* and gzip_* stuff from main domain + + # the nginx default is 1m, not enough for large media uploads + client_max_body_size 16m; + ignore_invalid_headers off; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location ~ ^/(media|proxy) { proxy_cache akkoma_media_cache; @@ -91,4 +130,8 @@ server { chunked_transfer_encoding on; proxy_pass http://phoenix; } + + location / { + return 404; + } } diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 5dedf276a..8dda1512d 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -130,6 +130,7 @@ def run(["get-packs" | args]) do } File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(pack_json, pretty: true)) + Pleroma.Emoji.reload() else IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"])) end @@ -235,6 +236,8 @@ def run(["gen-pack" | args]) do IO.puts("#{pack_file} has been created with the #{name} pack") end + + Pleroma.Emoji.reload() end def run(["reload"]) do diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 6a7d4f0d3..44f6b6e70 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -20,6 +20,7 @@ def run(["gen" | rest]) do output: :string, output_psql: :string, domain: :string, + media_url: :string, instance_name: :string, admin_email: :string, notify_email: :string, @@ -35,8 +36,7 @@ def run(["gen" | rest]) do listen_ip: :string, listen_port: :string, strip_uploads: :string, - anonymize_uploads: :string, - dedupe_uploads: :string + anonymize_uploads: :string ], aliases: [ o: :output, @@ -64,6 +64,14 @@ def run(["gen" | rest]) do ":" ) ++ [443] + media_url = + get_option( + options, + :media_url, + "What base url will uploads use? (e.g https://media.example.com/media)\n" <> + " Generally this should NOT use the same domain as the instance " + ) + name = get_option( options, @@ -186,14 +194,6 @@ def run(["gen" | rest]) do "n" ) === "y" - dedupe_uploads = - get_option( - options, - :dedupe_uploads, - "Do you want to deduplicate uploaded files? (y/n)", - "n" - ) === "y" - Config.put([:instance, :static_dir], static_dir) secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) @@ -207,6 +207,7 @@ def run(["gen" | rest]) do EEx.eval_file( template_dir <> "/sample_config.eex", domain: domain, + media_url: media_url, port: port, email: email, notify_email: notify_email, @@ -230,8 +231,7 @@ def run(["gen" | rest]) do upload_filters: upload_filters(%{ strip: strip_uploads, - anonymize: anonymize_uploads, - dedupe: dedupe_uploads + anonymize: anonymize_uploads }) ) @@ -319,13 +319,6 @@ defp upload_filters(filters) when is_map(filters) do enabled_filters end - enabled_filters = - if filters.dedupe do - enabled_filters ++ [Pleroma.Upload.Filter.Dedupe] - else - enabled_filters - end - enabled_filters end end diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 27a31afcf..299fb5b14 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -30,12 +30,12 @@ def run(["index"]) do meili_put( "/indexes/objects/settings/ranking-rules", [ - "published:desc", "words", - "exactness", "proximity", "typo", + "exactness", "attribute", + "published:desc", "sort" ] ) diff --git a/lib/mix/tasks/pleroma/security.ex b/lib/mix/tasks/pleroma/security.ex new file mode 100644 index 000000000..f039e0980 --- /dev/null +++ b/lib/mix/tasks/pleroma/security.ex @@ -0,0 +1,330 @@ +# Akkoma: Magically expressive social media +# Copyright © 2024 Akkoma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Security do + use Mix.Task + import Ecto.Query + import Mix.Pleroma + + alias Pleroma.Config + + require Logger + + @shortdoc """ + Security-related tasks, like e.g. checking for signs past exploits were abused. + """ + + # Constants etc + defp local_id_prefix(), do: Pleroma.Web.Endpoint.url() <> "/" + + defp local_id_pattern(), do: local_id_prefix() <> "%" + + @activity_exts ["activity+json", "activity%2Bjson"] + + defp activity_ext_url_patterns() do + for e <- @activity_exts do + for suf <- ["", "?%"] do + # Escape literal % for use in SQL patterns + ee = String.replace(e, "%", "\\%") + "%.#{ee}#{suf}" + end + end + |> List.flatten() + end + + # Search for malicious uploads exploiting the lack of Content-Type sanitisation from before 2024-03 + def run(["spoof-uploaded"]) do + Logger.put_process_level(self(), :notice) + start_pleroma() + + IO.puts(""" + +------------------------+ + | SPOOF SEARCH UPLOADS | + +------------------------+ + Checking if any uploads are using privileged types. + NOTE if attachment deletion is enabled, payloads used + in the past may no longer exist. + """) + + do_spoof_uploaded() + end + + # Fuzzy search for potentially counterfeit activities in the database resulting from the same exploit + def run(["spoof-inserted"]) do + Logger.put_process_level(self(), :notice) + start_pleroma() + + IO.puts(""" + +----------------------+ + | SPOOF SEARCH NOTES | + +----------------------+ + Starting fuzzy search for counterfeit activities. + NOTE this can not guarantee detecting all counterfeits + and may yield a small percentage of false positives. + """) + + do_spoof_inserted() + end + + # +-----------------------------+ + # | S P O O F - U P L O A D E D | + # +-----------------------------+ + defp do_spoof_uploaded() do + files = + case Config.get!([Pleroma.Upload, :uploader]) do + Pleroma.Uploaders.Local -> + uploads_search_spoofs_local_dir(Config.get!([Pleroma.Uploaders.Local, :uploads])) + + _ -> + IO.puts(""" + NOTE: + Not using local uploader; thus not affected by this exploit. + It's impossible to check for files, but in case local uploader was used before + or to check if anyone futilely attempted a spoof, notes will still be scanned. + """) + + [] + end + + emoji = uploads_search_spoofs_local_dir(Config.get!([:instance, :static_dir])) + + post_attachs = uploads_search_spoofs_notes() + + not_orphaned_urls = + post_attachs + |> Enum.map(fn {_u, _a, url} -> url end) + |> MapSet.new() + + orphaned_attachs = upload_search_orphaned_attachments(not_orphaned_urls) + + IO.puts("\nSearch concluded; here are the results:") + pretty_print_list_with_title(emoji, "Emoji") + pretty_print_list_with_title(files, "Uploaded Files") + pretty_print_list_with_title(post_attachs, "(Not Deleted) Post Attachments") + pretty_print_list_with_title(orphaned_attachs, "Orphaned Uploads") + + IO.puts(""" + In total found + #{length(emoji)} emoji + #{length(files)} uploads + #{length(post_attachs)} not deleted posts + #{length(orphaned_attachs)} orphaned attachments + """) + end + + defp uploads_search_spoofs_local_dir(dir) do + local_dir = String.replace_suffix(dir, "/", "") + + IO.puts("Searching for suspicious files in #{local_dir}...") + + glob_ext = "{" <> Enum.join(@activity_exts, ",") <> "}" + + Path.wildcard(local_dir <> "/**/*." <> glob_ext, match_dot: true) + |> Enum.map(fn path -> + String.replace_prefix(path, local_dir <> "/", "") + end) + |> Enum.sort() + end + + defp uploads_search_spoofs_notes() do + IO.puts("Now querying DB for posts with spoofing attachments. This might take a while...") + + patterns = [local_id_pattern() | activity_ext_url_patterns()] + + # if jsonb_array_elemsts in FROM can be used with normal Ecto functions, idk how + """ + SELECT DISTINCT a.data->>'actor', a.id, url->>'href' + FROM public.objects AS o JOIN public.activities AS a + ON o.data->>'id' = a.data->>'object', + jsonb_array_elements(o.data->'attachment') AS attachs, + jsonb_array_elements(attachs->'url') AS url + WHERE o.data->>'type' = 'Note' AND + o.data->>'id' LIKE $1::text AND ( + url->>'href' LIKE $2::text OR + url->>'href' LIKE $3::text OR + url->>'href' LIKE $4::text OR + url->>'href' LIKE $5::text + ) + ORDER BY a.data->>'actor', a.id, url->>'href'; + """ + |> Pleroma.Repo.query!(patterns, timeout: :infinity) + |> map_raw_id_apid_tuple() + end + + defp upload_search_orphaned_attachments(not_orphaned_urls) do + IO.puts(""" + Now querying DB for orphaned spoofing attachment (i.e. their post was deleted, + but if :cleanup_attachments was not enabled traces remain in the database) + This might take a bit... + """) + + patterns = activity_ext_url_patterns() + + """ + SELECT DISTINCT attach.id, url->>'href' + FROM public.objects AS attach, + jsonb_array_elements(attach.data->'url') AS url + WHERE (attach.data->>'type' = 'Image' OR + attach.data->>'type' = 'Document') + AND ( + url->>'href' LIKE $1::text OR + url->>'href' LIKE $2::text OR + url->>'href' LIKE $3::text OR + url->>'href' LIKE $4::text + ) + ORDER BY attach.id, url->>'href'; + """ + |> Pleroma.Repo.query!(patterns, timeout: :infinity) + |> then(fn res -> Enum.map(res.rows, fn [id, url] -> {id, url} end) end) + |> Enum.filter(fn {_, url} -> !(url in not_orphaned_urls) end) + end + + # +-----------------------------+ + # | S P O O F - I N S E R T E D | + # +-----------------------------+ + defp do_spoof_inserted() do + IO.puts(""" + Searching for local posts whose Create activity has no ActivityPub id... + This is a pretty good indicator, but only for spoofs of local actors + and only if the spoofing happened after around late 2021. + """) + + idless_create = + search_local_notes_without_create_id() + |> Enum.sort() + + IO.puts("Done.\n") + + IO.puts(""" + Now trying to weed out other poorly hidden spoofs. + This can't detect all and may have some false positives. + """) + + likely_spoofed_posts_set = MapSet.new(idless_create) + + sus_pattern_posts = + search_sus_notes_by_id_patterns() + |> Enum.filter(fn r -> !(r in likely_spoofed_posts_set) end) + + IO.puts("Done.\n") + + IO.puts(""" + Finally, searching for spoofed, local user accounts. + (It's impossible to detect spoofed remote users) + """) + + spoofed_users = search_bogus_local_users() + + pretty_print_list_with_title(sus_pattern_posts, "Maybe Spoofed Posts") + pretty_print_list_with_title(idless_create, "Likely Spoofed Posts") + pretty_print_list_with_title(spoofed_users, "Spoofed local user accounts") + + IO.puts(""" + In total found: + #{length(spoofed_users)} bogus users + #{length(idless_create)} likely spoofed posts + #{length(sus_pattern_posts)} maybe spoofed posts + """) + end + + defp search_local_notes_without_create_id() do + Pleroma.Object + |> where([o], fragment("?->>'id' LIKE ?", o.data, ^local_id_pattern())) + |> join(:inner, [o], a in Pleroma.Activity, + on: fragment("?->>'object' = ?->>'id'", a.data, o.data) + ) + |> where([o, a], fragment("NOT (? \\? 'id') OR ?->>'id' IS NULL", a.data, a.data)) + |> select([o, a], {a.id, fragment("?->>'id'", o.data)}) + |> order_by([o, a], a.id) + |> Pleroma.Repo.all(timeout: :infinity) + end + + defp search_sus_notes_by_id_patterns() do + [ep1, ep2, ep3, ep4] = activity_ext_url_patterns() + + Pleroma.Object + |> where( + [o], + # for local objects we know exactly how a genuine id looks like + # (though a thorough attacker can emulate this) + # for remote posts, use some best-effort patterns + fragment( + """ + (?->>'id' LIKE ? AND ?->>'id' NOT SIMILAR TO + ? || 'objects/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}') + """, + o.data, + ^local_id_pattern(), + o.data, + ^local_id_prefix() + ) or + fragment("?->>'id' LIKE ?", o.data, "%/emoji/%") or + fragment("?->>'id' LIKE ?", o.data, "%/media/%") or + fragment("?->>'id' LIKE ?", o.data, "%/proxy/%") or + fragment("?->>'id' LIKE ?", o.data, ^ep1) or + fragment("?->>'id' LIKE ?", o.data, ^ep2) or + fragment("?->>'id' LIKE ?", o.data, ^ep3) or + fragment("?->>'id' LIKE ?", o.data, ^ep4) + ) + |> join(:inner, [o], a in Pleroma.Activity, + on: fragment("?->>'object' = ?->>'id'", a.data, o.data) + ) + |> select([o, a], {a.id, fragment("?->>'id'", o.data)}) + |> order_by([o, a], a.id) + |> Pleroma.Repo.all(timeout: :infinity) + end + + defp search_bogus_local_users() do + Pleroma.User.Query.build(%{}) + |> where([u], u.local == false and like(u.ap_id, ^local_id_pattern())) + |> order_by([u], u.ap_id) + |> select([u], u.ap_id) + |> Pleroma.Repo.all(timeout: :infinity) + end + + # +-----------------------------------+ + # | module-specific utility functions | + # +-----------------------------------+ + defp pretty_print_list_with_title(list, title) do + title_len = String.length(title) + title_underline = String.duplicate("=", title_len) + IO.puts(title) + IO.puts(title_underline) + pretty_print_list(list) + end + + defp pretty_print_list([]), do: IO.puts("") + + defp pretty_print_list([{a, o} | rest]) + when (is_binary(a) or is_number(a)) and is_binary(o) do + IO.puts(" {#{a}, #{o}}") + pretty_print_list(rest) + end + + defp pretty_print_list([{u, a, o} | rest]) + when is_binary(a) and is_binary(u) and is_binary(o) do + IO.puts(" {#{u}, #{a}, #{o}}") + pretty_print_list(rest) + end + + defp pretty_print_list([e | rest]) when is_binary(e) do + IO.puts(" #{e}") + pretty_print_list(rest) + end + + defp pretty_print_list([e | rest]), do: pretty_print_list([inspect(e) | rest]) + + defp map_raw_id_apid_tuple(res) do + user_prefix = local_id_prefix() <> "users/" + + Enum.map(res.rows, fn + [uid, aid, oid] -> + { + String.replace_prefix(uid, user_prefix, ""), + FlakeId.to_string(aid), + oid + } + end) + end +end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 4ca1c28eb..1a8e866ef 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -11,6 +11,7 @@ defmodule Mix.Tasks.Pleroma.User do alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline + use Pleroma.Web, :verified_routes @shortdoc "Manages Pleroma users" @moduledoc File.read!("docs/docs/administration/CLI_tasks/user.md") @@ -113,11 +114,7 @@ def run(["reset_password", nickname]) do {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do shell_info("Generated password reset token for #{user.nickname}") - IO.puts( - "URL: #{Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint, - :reset, - token.token)}" - ) + IO.puts("URL: #{~p[/api/v1/pleroma/password_reset/#{token.token}]}") else _ -> shell_error("No local user #{nickname}") @@ -303,13 +300,7 @@ def run(["invite" | rest]) do {:ok, invite} <- UserInviteToken.create_invite(options) do shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " ")) - url = - Pleroma.Web.Router.Helpers.redirect_url( - Pleroma.Web.Endpoint, - :registration_page, - invite.token - ) - + url = url(~p[/registration/#{invite.token}]) IO.puts(url) else error -> diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex index 8ed64eb16..72def9dff 100644 --- a/lib/phoenix/transports/web_socket/raw.ex +++ b/lib/phoenix/transports/web_socket/raw.ex @@ -26,7 +26,6 @@ def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do conn |> fetch_query_params |> Transport.transport_log(opts[:transport_log]) - |> Transport.force_ssl(handler, endpoint, opts) |> Transport.check_origin(handler, endpoint, opts) case conn do diff --git a/lib/pleroma/activity/pruner.ex b/lib/pleroma/activity/pruner.ex index 7f561ebae..54a40b534 100644 --- a/lib/pleroma/activity/pruner.ex +++ b/lib/pleroma/activity/pruner.ex @@ -26,6 +26,15 @@ def prune_undos do |> Repo.delete_all(timeout: :infinity) end + def prune_updates do + before_time = cutoff() + + from(a in Activity, + where: fragment("?->>'type' = ?", a.data, "Update") and a.inserted_at < ^before_time + ) + |> Repo.delete_all(timeout: :infinity) + end + def prune_removes do before_time = cutoff() diff --git a/lib/pleroma/config/release_runtime_provider.ex b/lib/pleroma/config/release_runtime_provider.ex index a829a0206..d8818dfee 100644 --- a/lib/pleroma/config/release_runtime_provider.ex +++ b/lib/pleroma/config/release_runtime_provider.ex @@ -23,7 +23,7 @@ def load(config, opts) do with_runtime_config = if File.exists?(config_path) do # - %File.Stat{mode: mode} = File.lstat!(config_path) + %File.Stat{mode: mode} = File.stat!(config_path) if Bitwise.band(mode, 0o007) > 0 do raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}" diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index 88bc78aec..683de8e3b 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -6,10 +6,13 @@ defmodule Pleroma.Emails.AdminEmail do @moduledoc "Admin emails" import Swoosh.Email - + use Pleroma.Web, :mailer alias Pleroma.Config alias Pleroma.HTML - alias Pleroma.Web.Router.Helpers + + use Phoenix.VerifiedRoutes, + endpoint: Pleroma.Web.Endpoint, + router: Pleroma.Web.Router defp instance_config, do: Config.get(:instance) defp instance_name, do: instance_config()[:name] @@ -45,7 +48,7 @@ def report(to, reporter, account, statuses, comment) do statuses |> Enum.map(fn %{id: id} -> - status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id) + status_url = url(~p[/notice/#{id}]) "
  • #{status_url}
  • " %{"id" => id} when is_binary(id) -> diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex index d42236c5e..6a79a7694 100644 --- a/lib/pleroma/emails/mailer.ex +++ b/lib/pleroma/emails/mailer.ex @@ -55,12 +55,61 @@ def deliver!(email, config) do @doc false def validate_dependency do - parse_config([]) + parse_config([], defaults: false) |> Keyword.get(:adapter) |> Swoosh.Mailer.validate_dependency() end - defp parse_config(config) do - Swoosh.Mailer.parse_config(@otp_app, __MODULE__, @mailer_config, config) + defp ensure_charlist(input) do + case input do + i when is_binary(i) -> String.to_charlist(input) + i when is_list(i) -> i + end + end + + defp default_config(adapter, conf, opts) + + defp default_config(_, _, defaults: false) do + [] + end + + defp default_config(Swoosh.Adapters.SMTP, conf, _) do + # gen_smtp and Erlang's tls defaults are very barebones, if nothing is set. + # Add sane defaults for our usecase to make config less painful for admins + relay = ensure_charlist(Keyword.get(conf, :relay)) + ssl_disabled = Keyword.get(conf, :ssl) === false + os_cacerts = :public_key.cacerts_get() + + common_tls_opts = [ + cacerts: os_cacerts, + versions: [:"tlsv1.2", :"tlsv1.3"], + verify: :verify_peer, + # some versions have supposedly issues verifying wildcard certs without this + server_name_indication: relay, + # the default of 10 is too restrictive + depth: 32 + ] + + [ + auth: :always, + no_mx_lookups: false, + # Direct SSL/TLS + # (if ssl was explicitly disabled, we must not pass TLS options to the socket) + ssl: true, + sockopts: if(ssl_disabled, do: [], else: common_tls_opts), + # STARTTLS upgrade (can't be set to :always when already using direct TLS) + tls: :if_available, + tls_options: common_tls_opts + ] + end + + defp default_config(_, _, _), do: [] + + defp parse_config(config, opts \\ []) do + conf = Swoosh.Mailer.parse_config(@otp_app, __MODULE__, @mailer_config, config) + adapter = Keyword.get(conf, :adapter) + + default_config(adapter, conf, opts) + |> Keyword.merge(conf) end end diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 1588c099c..a7bcc03b6 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -6,12 +6,11 @@ defmodule Pleroma.Emails.UserEmail do @moduledoc "User emails" require Pleroma.Web.Gettext + use Pleroma.Web, :mailer alias Pleroma.Config alias Pleroma.User - alias Pleroma.Web.Endpoint alias Pleroma.Web.Gettext - alias Pleroma.Web.Router import Swoosh.Email import Phoenix.Swoosh, except: [render_body: 3] @@ -75,7 +74,7 @@ def welcome(user, opts \\ %{}) do def password_reset_email(user, token) when is_binary(token) do Gettext.with_locale_or_default user.language do - password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token) + password_reset_url = url(~p[/api/v1/pleroma/password_reset/#{token}]) html_body = Gettext.dpgettext( @@ -108,12 +107,7 @@ def user_invitation_email( to_name \\ nil ) do Gettext.with_locale_or_default user.language do - registration_url = - Router.Helpers.redirect_url( - Endpoint, - :registration_page, - user_invite_token.token - ) + registration_url = url(~p[/registration/#{user_invite_token.token}]) html_body = Gettext.dpgettext( @@ -146,13 +140,7 @@ def user_invitation_email( def account_confirmation_email(user) do Gettext.with_locale_or_default user.language do - confirmation_url = - Router.Helpers.confirm_email_url( - Endpoint, - :confirm_email, - user.id, - to_string(user.confirmation_token) - ) + confirmation_url = url(~p[/api/account/confirm_email/#{user.id}/#{user.confirmation_token}]) html_body = Gettext.dpgettext( @@ -342,7 +330,7 @@ def unsubscribe_url(user, notifications_type) do |> Pleroma.JWT.generate_and_sign!() |> Base.encode64() - Router.Helpers.subscription_url(Endpoint, :unsubscribe, token) + url(~p[/mailer/unsubscribe/#{token}]) end def backup_is_ready_email(backup, admin_user_id \\ nil) do diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 9049d9097..142208854 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -26,12 +26,37 @@ defmodule Pleroma.Emoji.Pack do alias Pleroma.Emoji.Pack alias Pleroma.Utils + # Invalid/Malicious names are supposed to be filtered out before path joining, + # but there are many entrypoints to affected functions so as the code changes + # we might accidentally let an unsanitised name slip through. + # To make sure, use the below which crash the process otherwise. + + # ALWAYS use this when constructing paths from external name! + # (name meaning it must be only a single path component) + defp path_join_name_safe(dir, name) do + if to_string(name) != Path.basename(name) or name in ["..", ".", ""] do + raise "Invalid or malicious pack name: #{name}" + else + Path.join(dir, name) + end + end + + # ALWAYS use this to join external paths + # (which are allowed to have several components) + defp path_join_safe(dir, path) do + {:ok, safe_path} = Path.safe_relative(path) + Path.join(dir, safe_path) + end + @spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} def create(name) do with :ok <- validate_not_empty([name]), - dir <- Path.join(emoji_path(), name), + dir <- path_join_name_safe(emoji_path(), name), :ok <- File.mkdir(dir) do - save_pack(%__MODULE__{pack_file: Path.join(dir, "pack.json")}) + save_pack(%__MODULE__{ + path: dir, + pack_file: Path.join(dir, "pack.json") + }) end end @@ -65,7 +90,7 @@ def show(opts) do {:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values} def delete(name) do with :ok <- validate_not_empty([name]), - pack_path <- Path.join(emoji_path(), name) do + pack_path <- path_join_name_safe(emoji_path(), name) do File.rm_rf(pack_path) end end @@ -89,7 +114,7 @@ defp unpack_zip_emojies(zip_files) do end) end - @spec add_file(t(), String.t(), Path.t(), Plug.Upload.t()) :: + @spec add_file(t(), String.t(), Path.t(), Plug.Upload.t() | binary()) :: {:ok, t()} | {:error, File.posix() | atom()} def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do @@ -107,7 +132,7 @@ def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} Enum.map_reduce(emojies, pack, fn item, emoji_pack -> emoji_file = %Plug.Upload{ filename: item[:filename], - path: Path.join(tmp_dir, item[:path]) + path: path_join_safe(tmp_dir, item[:path]) } {:ok, updated_pack} = @@ -137,6 +162,14 @@ def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} end def add_file(%Pack{} = pack, shortcode, filename, %Plug.Upload{} = file) do + try_add_file(pack, shortcode, filename, file) + end + + def add_file(%Pack{} = pack, shortcode, filename, filedata) when is_binary(filedata) do + try_add_file(pack, shortcode, filename, filedata) + end + + defp try_add_file(%Pack{} = pack, shortcode, filename, file) do with :ok <- validate_not_empty([shortcode, filename]), :ok <- validate_emoji_not_exists(shortcode), {:ok, updated_pack} <- do_add_file(pack, shortcode, filename, file) do @@ -189,6 +222,7 @@ def import_from_filesystem do {:ok, results} <- File.ls(emoji_path) do names = results + # items come from File.ls, thus safe |> Enum.map(&Path.join(emoji_path, &1)) |> Enum.reject(fn path -> File.dir?(path) and File.exists?(Path.join(path, "pack.json")) @@ -287,8 +321,8 @@ def update_metadata(name, data) do @spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()} def load_pack(name) do - name = Path.basename(name) - pack_file = Path.join([emoji_path(), name, "pack.json"]) + pack_dir = path_join_name_safe(emoji_path(), name) + pack_file = Path.join(pack_dir, "pack.json") with {:ok, _} <- File.stat(pack_file), {:ok, pack_data} <- File.read(pack_file) do @@ -412,7 +446,13 @@ defp downloadable?(pack) do end defp create_archive_and_cache(pack, hash) do - files = [~c"pack.json" | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)] + files = [ + ~c"pack.json" + | Enum.map(pack.files, fn {_, file} -> + {:ok, file} = Path.safe_relative(file) + to_charlist(file) + end) + ] {:ok, {_, result}} = :zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)]) @@ -474,7 +514,7 @@ defp validate_not_empty(list) do end defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do - file_path = Path.join(pack.path, filename) + file_path = path_join_safe(pack.path, filename) create_subdirs(file_path) with {:ok, _} <- File.copy(upload_path, file_path) do @@ -482,6 +522,12 @@ defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do end end + defp save_file(file_data, pack, filename) when is_binary(file_data) do + file_path = path_join_safe(pack.path, filename) + create_subdirs(file_path) + File.write(file_path, file_data, [:binary]) + end + defp put_emoji(pack, shortcode, filename) do files = Map.put(pack.files, shortcode, filename) %{pack | files: files, files_count: length(Map.keys(files))} @@ -493,8 +539,8 @@ defp delete_emoji(pack, shortcode) do end defp rename_file(pack, filename, new_filename) do - old_path = Path.join(pack.path, filename) - new_path = Path.join(pack.path, new_filename) + old_path = path_join_safe(pack.path, filename) + new_path = path_join_safe(pack.path, new_filename) create_subdirs(new_path) with :ok <- File.rename(old_path, new_path) do @@ -512,7 +558,7 @@ defp create_subdirs(file_path) do defp remove_file(pack, shortcode) do with {:ok, filename} <- get_filename(pack, shortcode), - emoji <- Path.join(pack.path, filename), + emoji <- path_join_safe(pack.path, filename), :ok <- File.rm(emoji) do remove_dir_if_empty(emoji, filename) end @@ -530,7 +576,7 @@ defp remove_dir_if_empty(emoji, filename) do defp get_filename(pack, shortcode) do with %{^shortcode => filename} when is_binary(filename) <- pack.files, - file_path <- Path.join(pack.path, filename), + file_path <- path_join_safe(pack.path, filename), {:ok, _} <- File.stat(file_path) do {:ok, filename} else @@ -568,7 +614,7 @@ defp validate_downloadable(pack) do end defp copy_as(remote_pack, local_name) do - path = Path.join(emoji_path(), local_name) + path = path_join_name_safe(emoji_path(), local_name) %__MODULE__{ name: local_name, diff --git a/lib/pleroma/job_queue_monitor.ex b/lib/pleroma/job_queue_monitor.ex index b5f124923..8d81ffcac 100644 --- a/lib/pleroma/job_queue_monitor.ex +++ b/lib/pleroma/job_queue_monitor.ex @@ -15,8 +15,19 @@ def start_link(_) do @impl true def init(state) do - :telemetry.attach("oban-monitor-failure", [:oban, :job, :exception], &handle_event/4, nil) - :telemetry.attach("oban-monitor-success", [:oban, :job, :stop], &handle_event/4, nil) + :telemetry.attach( + "oban-monitor-failure", + [:oban, :job, :exception], + &Pleroma.JobQueueMonitor.handle_event/4, + nil + ) + + :telemetry.attach( + "oban-monitor-success", + [:oban, :job, :stop], + &Pleroma.JobQueueMonitor.handle_event/4, + nil + ) {:ok, state} end diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 040537acf..37bc20e4d 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -11,6 +11,9 @@ defmodule Pleroma.Object.Containment do Object containment is an important step in validating remote objects to prevent spoofing, therefore removal of object containment functions is NOT recommended. """ + + alias Pleroma.Web.ActivityPub.Transmogrifier + def get_actor(%{"actor" => actor}) when is_binary(actor) do actor end @@ -47,6 +50,31 @@ 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 compare_uris_exact(%URI{} = id, %URI{} = other), + do: compare_uris_exact(URI.to_string(id), URI.to_string(other)) + + defp compare_uris_exact(id_uri, other_uri) + when is_binary(id_uri) and is_binary(other_uri) do + norm_id = String.replace_suffix(id_uri, "/", "") + norm_other = String.replace_suffix(other_uri, "/", "") + if norm_id == norm_other, do: :ok, else: :error + end + + @doc """ + Checks whether an URL to fetch from is from the local server. + + We never want to fetch from ourselves; if it’s not in the database + it can’t be authentic and must be a counterfeit. + """ + def contain_local_fetch(id) do + case compare_uris(URI.parse(id), Pleroma.Web.Endpoint.struct_url()) do + :ok -> :error + _ -> :ok + end + end + @doc """ Checks that an imported AP object's actor matches the host it came from. """ @@ -62,8 +90,31 @@ def contain_origin(id, %{"actor" => _actor} = params) do def contain_origin(id, %{"attributedTo" => actor} = params), do: contain_origin(id, Map.put(params, "actor", actor)) - def contain_origin(_id, _data), do: :error + def contain_origin(_id, _data), do: :ok + @doc """ + Check whether the fetch URL (after redirects) exactly (sans tralining slash) matches either + the canonical ActivityPub id or the objects url field (for display URLs from *key and Mastodon) + + Since this is meant to be used for fetches, anonymous or transient objects are not accepted here. + """ + def contain_id_to_fetch(url, %{"id" => id} = data) when is_binary(id) do + with {:id, :error} <- {:id, compare_uris_exact(id, url)}, + # "url" can be a "Link" object and this is checked before full normalisation + display_url <- Transmogrifier.fix_url(data)["url"], + true <- display_url != nil do + compare_uris_exact(display_url, url) + else + {:id, :ok} -> :ok + _ -> :error + end + end + + def contain_id_to_fetch(_url, _data), do: :error + + @doc """ + Check whether the object id is from the same host as another id + """ def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do id_uri = URI.parse(id) other_uri = URI.parse(other_id) @@ -85,4 +136,12 @@ def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}), do: contain_origin(id, object) def contain_child(_), do: :ok + + @doc "Checks whether two URIs belong to the same domain" + def same_origin(id1, id2) do + uri1 = URI.parse(id1) + uri2 = URI.parse(id2) + + compare_uris(uri1, uri2) + end end diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 9e62ca69f..6609b8c1a 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -18,6 +18,16 @@ defmodule Pleroma.Object.Fetcher do require Logger require Pleroma.Constants + @moduledoc """ + This module deals with correctly fetching Acitivity Pub objects in a safe way. + + The core function is `fetch_and_contain_remote_object_from_id/1` which performs + the actual fetch and common safety and authenticity checks. Other `fetch_*` + function use the former and perform some additional tasks + """ + + @mix_env Mix.env() + defp touch_changeset(changeset) do updated_at = NaiveDateTime.utc_now() @@ -103,18 +113,26 @@ defp reinject_object(%Object{} = object, new_data) do end end + @doc "Assumes object already is in our database and refetches from remote to update (e.g. for polls)" def refetch_object(%Object{data: %{"id" => id}} = object) do with {:local, false} <- {:local, Object.local?(object)}, {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id), + {:id, true} <- {:id, new_data["id"] == id}, {:ok, object} <- reinject_object(object, new_data) do {:ok, object} else {:local, true} -> {:ok, object} + {:id, false} -> {:error, "Object id changed on refetch"} e -> {:error, e} end end - # Note: will create a Create activity, which we need internally at the moment. + @doc """ + Fetches a new object and puts it through the processing pipeline for inbound objects + + Note: will also insert a fake Create activity, since atm we internally + need everything to be traced back to a Create activity. + """ def fetch_object_from_id(id, options \\ []) do with %URI{} = uri <- URI.parse(id), # let's check the URI is even vaguely valid first @@ -127,7 +145,6 @@ def fetch_object_from_id(id, options \\ []) do {_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)}, {_, nil} <- {:normalize, Object.normalize(data, fetch: false)}, params <- prepare_activity_params(data), - {_, :ok} <- {:containment, Containment.contain_origin(id, params)}, {_, {:ok, activity}} <- {:transmogrifier, Transmogrifier.handle_incoming(params, options)}, {_, _data, %Object{} = object} <- @@ -140,9 +157,6 @@ def fetch_object_from_id(id, options \\ []) do {:scheme, false} -> {:error, "URI Scheme Invalid"} - {:containment, _} -> - {:error, "Object containment failed."} - {:transmogrifier, {:error, {:reject, e}}} -> {:reject, e} @@ -185,6 +199,7 @@ defp prepare_activity_params(data) do |> Maps.put_if_present("bcc", data["bcc"]) end + @doc "Identical to `fetch_object_from_id/2` but just directly returns the object or on error `nil`" def fetch_object_from_id!(id, options \\ []) do with {:ok, object} <- fetch_object_from_id(id, options) do object @@ -235,6 +250,7 @@ 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) def fetch_and_contain_remote_object_from_id(%{"id" => id}), @@ -244,18 +260,29 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do Logger.debug("Fetching object #{id} via AP") with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")}, - {:ok, body} <- get_object(id), + {_, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)}, + {:ok, final_id, body} <- get_object(id), {:ok, data} <- safe_json_decode(body), - :ok <- Containment.contain_origin_from_id(id, data) do - unless Instances.reachable?(id) do - Instances.set_reachable(id) + {_, :ok} <- {:strict_id, Containment.contain_id_to_fetch(final_id, data)}, + {_, :ok} <- {:containment, Containment.contain_origin(final_id, data)} do + unless Instances.reachable?(final_id) do + Instances.set_reachable(final_id) end {:ok, data} else + {:strict_id, _} -> + {:error, "Object's ActivityPub id/url does not match final fetch URL"} + {:scheme, _} -> {:error, "Unsupported URI scheme"} + {:local_fetch, _} -> + {:error, "Trying to fetch local resource"} + + {:containment, _} -> + {:error, "Object containment failed."} + {:error, e} -> {:error, e} @@ -267,6 +294,32 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"} + defp check_crossdomain_redirect(final_host, original_url) + + # HOPEFULLY TEMPORARY + # Basically none of our Tesla mocks in tests set the (supposed to + # exist for Tesla proper) url parameter for their responses + # causing almost every fetch in test to fail otherwise + if @mix_env == :test do + defp check_crossdomain_redirect(nil, _) do + {:cross_domain_redirect, false} + end + end + + defp check_crossdomain_redirect(final_host, original_url) do + {:cross_domain_redirect, final_host != URI.parse(original_url).host} + end + + if @mix_env == :test do + defp get_final_id(nil, initial_url), do: initial_url + defp get_final_id("", initial_url), do: initial_url + end + + defp get_final_id(final_url, _intial_url) do + final_url + end + + @doc "Do NOT use; only public for use in tests" def get_object(id) do date = Pleroma.Signature.signed_date() @@ -275,37 +328,42 @@ def get_object(id) do |> maybe_date_fetch(date) |> sign_fetch(id, date) - case HTTP.get(id, headers) do - {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 -> - case List.keyfind(headers, "content-type", 0) do - {_, content_type} -> - case Plug.Conn.Utils.media_type(content_type) do - {:ok, "application", "activity+json", _} -> - {:ok, body} + with {:ok, %{body: body, status: code, headers: headers, url: final_url}} + when code in 200..299 <- + HTTP.get(id, headers), + remote_host <- + URI.parse(final_url).host, + {:cross_domain_redirect, false} <- + check_crossdomain_redirect(remote_host, id), + {:has_content_type, {_, content_type}} <- + {:has_content_type, List.keyfind(headers, "content-type", 0)}, + {:parse_content_type, {:ok, "application", subtype, type_params}} <- + {:parse_content_type, Plug.Conn.Utils.media_type(content_type)} do + final_id = get_final_id(final_url, id) - {:ok, "application", "ld+json", - %{"profile" => "https://www.w3.org/ns/activitystreams"}} -> - {:ok, body} + case {subtype, type_params} do + {"activity+json", _} -> + {:ok, final_id, body} - # pixelfed sometimes (and only sometimes) responds with http instead of https - {:ok, "application", "ld+json", - %{"profile" => "http://www.w3.org/ns/activitystreams"}} -> - {:ok, body} - - _ -> - {:error, {:content_type, content_type}} - end - - _ -> - {:error, {:content_type, nil}} - end + {"ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} -> + {:ok, final_id, body} + _ -> + {:error, {:content_type, content_type}} + end + else {:ok, %{status: code}} when code in [404, 410] -> {:error, {"Object has been deleted", id, code}} {:error, e} -> {:error, e} + {:has_content_type, _} -> + {:error, {:content_type, nil}} + + {:parse_content_type, e} -> + {:error, {:content_type, e}} + e -> {:error, e} end diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index b44f0b90a..f017bf51b 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -17,6 +17,8 @@ defmodule Pleroma.ReverseProxy do @failed_request_ttl :timer.seconds(60) @methods ~w(GET HEAD) + @allowed_mime_types Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types], []) + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) def max_read_duration_default, do: @max_read_duration @@ -61,11 +63,14 @@ def default_cache_control_header, do: @default_cache_control_header """ @inline_content_types [ + "image/avif", "image/gif", "image/jpeg", "image/jpg", + "image/jxl", "image/png", "image/svg+xml", + "image/webp", "audio/mpeg", "audio/mp3", "video/webm", @@ -250,6 +255,7 @@ defp build_resp_headers(headers, opts) do headers |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end) |> build_resp_cache_headers(opts) + |> sanitise_content_type() |> build_resp_content_disposition_header(opts) |> build_csp_headers() |> Keyword.merge(Keyword.get(opts, :resp_headers, [])) @@ -279,6 +285,21 @@ defp build_resp_cache_headers(headers, _opts) do end end + defp sanitise_content_type(headers) do + original_ct = get_content_type(headers) + + safe_ct = + Pleroma.Web.Plugs.Utils.get_safe_mime_type( + %{allowed_mime_types: @allowed_mime_types}, + original_ct + ) + + [ + {"content-type", safe_ct} + | Enum.filter(headers, fn {k, _v} -> k != "content-type" end) + ] + end + defp build_resp_content_disposition_header(headers, opts) do opt = Keyword.get(opts, :inline_content_types, @inline_content_types) diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index b229e6296..3d33fcd62 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -76,6 +76,6 @@ def sign(%User{keys: keys} = user, headers) do def signed_date, do: signed_date(NaiveDateTime.utc_now()) def signed_date(%NaiveDateTime{} = date) do - Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + Timex.lformat!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT", "en") end end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 99b6b5215..1158e9449 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -39,6 +39,8 @@ defmodule Pleroma.Upload do alias Pleroma.Web.ActivityPub.Utils require Logger + @mix_env Mix.env() + @type source :: Plug.Upload.t() | (data_uri_string :: String.t()) @@ -64,7 +66,7 @@ defmodule Pleroma.Upload do path: String.t() } - @always_enabled_filters [Pleroma.Upload.Filter.AnonymizeFilename] + @always_enabled_filters [Pleroma.Upload.Filter.Dedupe] defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path] @@ -228,6 +230,13 @@ defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do defp url_from_spec(_upload, _base_url, {:url, url}), do: url + if @mix_env == :test do + defp choose_base_url(prim, sec \\ nil), + do: prim || sec || Pleroma.Web.Endpoint.url() <> "/media/" + else + defp choose_base_url(prim, sec \\ nil), do: prim || sec + end + def base_url do uploader = Config.get([Pleroma.Upload, :uploader]) upload_base_url = Config.get([Pleroma.Upload, :base_url]) @@ -235,7 +244,7 @@ def base_url do case uploader do Pleroma.Uploaders.Local -> - upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/" + choose_base_url(upload_base_url) Pleroma.Uploaders.S3 -> bucket = Config.get([Pleroma.Uploaders.S3, :bucket]) @@ -261,7 +270,7 @@ def base_url do end _ -> - public_endpoint || upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/" + choose_base_url(public_endpoint, upload_base_url) end end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 83b45e3b4..8449af620 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -44,6 +44,8 @@ defmodule Pleroma.User do alias Pleroma.Web.RelMe alias Pleroma.Workers.BackgroundWorker + use Pleroma.Web, :verified_routes + require Logger @type t :: %__MODULE__{} @@ -158,6 +160,7 @@ defmodule Pleroma.User do field(:last_status_at, :naive_datetime) field(:language, :string) field(:status_ttl_days, :integer, default: nil) + field(:permit_followback, :boolean, default: false) field(:accepts_direct_messages_from, Ecto.Enum, values: [:everybody, :people_i_follow, :nobody], @@ -379,6 +382,10 @@ def banner_url(user, options \\ []) do do_optional_url(user.banner, "#{Endpoint.url()}/images/banner.png", options) end + def background_url(user) do + do_optional_url(user.background, nil, no_default: true) + end + defp do_optional_url(field, default, options) do case field do %{"url" => [%{"href" => href} | _]} when is_binary(href) -> @@ -463,6 +470,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :avatar, :ap_enabled, :banner, + :background, :is_locked, :last_refreshed_at, :uri, @@ -542,6 +550,7 @@ def update_changeset(struct, params \\ %{}) do :actor_type, :disclose_client, :status_ttl_days, + :permit_followback, :accepts_direct_messages_from ] ) @@ -970,16 +979,21 @@ def needs_update?(%User{local: false} = user) do def needs_update?(_), do: true + # "Locked" (self-locked) users demand explicit authorization of follow requests + @spec can_direct_follow_local(User.t(), User.t()) :: true | false + def can_direct_follow_local(%User{} = follower, %User{local: true} = followed) do + !followed.is_locked || (followed.permit_followback and is_friend_of(follower, followed)) + end + @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()} - # "Locked" (self-locked) users demand explicit authorization of follow requests - def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do - follow(follower, followed, :follow_pending) - end - def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do - follow(follower, followed) + if can_direct_follow_local(follower, followed) do + follow(follower, followed) + else + follow(follower, followed, :follow_pending) + end end def maybe_direct_follow(%User{} = follower, %User{} = followed) do @@ -1329,6 +1343,13 @@ def get_friends_ids(%User{} = user, page \\ nil) do |> Repo.all() end + def is_friend_of(%User{} = potential_friend, %User{local: true} = user) do + user + |> get_friends_query() + |> where(id: ^potential_friend.id) + |> Repo.exists?() + end + def increase_note_count(%User{} = user) do User |> where(id: ^user.id) @@ -1603,9 +1624,13 @@ def blocks_user?(%User{} = user, %User{} = target) do def blocks_user?(_, _), do: false def blocks_domain?(%User{} = user, %User{} = target) do - domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) %{host: host} = URI.parse(target.ap_id) - Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) + Enum.member?(user.domain_blocks, host) + # TODO: functionality should probably be changed such that subdomains block as well, + # but as it stands, this just hecks up the relationships endpoint + # domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) + # %{host: host} = URI.parse(target.ap_id) + # Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) end def blocks_domain?(_, _), do: false @@ -2447,12 +2472,7 @@ defp validate_rel_me_field(changeset, fields, raw_fields, %User{ end if is_url(raw_value) do - frontend_url = - Pleroma.Web.Router.Helpers.redirect_url( - Pleroma.Web.Endpoint, - :redirector_with_meta, - nickname - ) + frontend_url = url(~p[/#{nickname}]) possible_urls = [ap_id, frontend_url] diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index ecd98b6ca..5422e7896 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web do alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper + require Pleroma.Constants def controller do quote do @@ -37,7 +38,7 @@ def controller do import Pleroma.Web.Gettext import Pleroma.Web.TranslationHelpers - alias Pleroma.Web.Router.Helpers, as: Routes + unquote(verified_routes()) plug(:set_put_layout) @@ -184,7 +185,10 @@ def view do # Import convenience functions from controllers import Phoenix.Controller, - only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] + only: [view_module: 1, view_template: 1] + + import Phoenix.Flash + alias Phoenix.Flash # Include shared imports and aliases for views unquote(view_helpers()) @@ -218,7 +222,7 @@ def component do def router do quote do - use Phoenix.Router + use Phoenix.Router, helpers: false import Plug.Conn import Phoenix.Controller @@ -246,7 +250,24 @@ defp view_helpers do import Pleroma.Web.ErrorHelpers import Pleroma.Web.Gettext - alias Pleroma.Web.Router.Helpers, as: Routes + unquote(verified_routes()) + end + end + + def static_paths, do: Pleroma.Constants.static_only_files() + + def verified_routes do + quote do + use Phoenix.VerifiedRoutes, + endpoint: Pleroma.Web.Endpoint, + router: Pleroma.Web.Router, + statics: Pleroma.Web.static_paths() + end + end + + def mailer do + quote do + unquote(verified_routes()) end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 649bf9095..1e06bc809 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -22,6 +22,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Upload alias Pleroma.User alias Pleroma.Web.ActivityPub.MRF + alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.Streamer alias Pleroma.Web.WebFinger @@ -1603,6 +1604,7 @@ defp object_to_user_data(data, additional) do uri: get_actor_url(data["url"]), ap_enabled: true, banner: normalize_image(data["image"]), + background: normalize_image(data["backgroundUrl"]), fields: fields, emoji: emojis, is_locked: is_locked, @@ -1721,6 +1723,7 @@ def user_data_from_user_object(data, additional \\ []) do def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), + {:valid, {:ok, _, _}} <- {:valid, UserValidator.validate(data, [])}, {:ok, data} <- user_data_from_user_object(data, additional) do {:ok, maybe_update_follow_information(data)} else @@ -1733,6 +1736,10 @@ def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do Logger.debug("Rejected user #{ap_id}: #{inspect(reason)}") {:error, e} + {:valid, reason} -> + Logger.debug("Data is not a valid user #{ap_id}: #{inspect(reason)}") + {:error, "Not a user"} + {:error, e} -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") {:error, e} @@ -1792,6 +1799,11 @@ def pin_data_from_featured_collection( end) end + def pin_data_from_featured_collection(obj) do + Logger.error("Could not parse featured collection #{inspect(obj)}") + %{} + end + def fetch_and_prepare_featured_from_ap_id(nil) do {:ok, %{}} end @@ -1828,6 +1840,13 @@ def make_user_from_ap_id(ap_id, additional \\ []) do with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do {:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end) + 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) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 6d39ad3a8..e67a14b58 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -18,6 +18,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do alias Pleroma.Web.CommonAPI.ActivityDraft alias Pleroma.Web.Endpoint + use Pleroma.Web, :verified_routes + require Pleroma.Constants def accept_or_reject(actor, activity, type) do @@ -402,6 +404,6 @@ def unpin(%User{} = user, object) do end defp pinned_url(nickname) when is_binary(nickname) do - Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname) + url(~p[/users/#{nickname}/collections/featured]) end end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index ba54eb674..0b8b846ec 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -178,6 +178,23 @@ defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image defp check_banner_removal(_actor_info, object), do: {:ok, object} + defp check_background_removal( + %{host: actor_host} = _actor_info, + %{"backgroundUrl" => _bg} = object + ) do + background_removal = + instance_list(:background_removal) + |> MRF.subdomains_regex() + + if MRF.subdomain_match?(background_removal, actor_host) do + {:ok, Map.delete(object, "backgroundUrl")} + else + {:ok, object} + end + end + + defp check_background_removal(_actor_info, object), do: {:ok, object} + defp extract_context_uri(%{"conversation" => "tag:" <> rest}) do rest |> String.split(",", parts: 2, trim: true) @@ -283,7 +300,8 @@ def filter(%{"id" => actor, "type" => obj_type} = object) with {:ok, _} <- check_accept(actor_info), {:ok, _} <- check_reject(actor_info), {:ok, object} <- check_avatar_removal(actor_info, object), - {:ok, object} <- check_banner_removal(actor_info, object) do + {:ok, object} <- check_banner_removal(actor_info, object), + {:ok, object} <- check_background_removal(actor_info, object) do {:ok, object} else {:reject, nil} -> {:reject, "[SimplePolicy]"} @@ -314,6 +332,20 @@ def filter(object) when is_binary(object) do def filter(object), do: {:ok, object} defp obfuscate(string) when is_binary(string) do + # Want to strip at least two neighbouring chars + # to ensure at least one non-dot char is in the obfuscation area + stripped = String.length(string) - 6 + + {keepstart, keepend} = + if stripped > 1 do + {3, 3} + else + { + 2 - div(1 - stripped, 2), + 2 + div(stripped, 2) + } + end + string |> to_charlist() |> Enum.with_index() @@ -322,7 +354,7 @@ defp obfuscate(string) when is_binary(string) do ?. {char, index} -> - if 3 <= index && index < String.length(string) - 3, do: ?*, else: char + if keepstart <= index && index < String.length(string) - keepend, do: ?*, else: char end) |> to_string() end @@ -433,6 +465,11 @@ def config_description do key: :banner_removal, description: "List of instances to strip banners from and the reason for doing so" }, + %{ + key: :background_removal, + description: + "List of instances to strip user backgrounds from and the reason for doing so" + }, %{ key: :reject_deletes, description: "List of instances to reject deletions from and the reason for doing so" diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex index 45b2d4ca7..26d3dc592 100644 --- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex @@ -6,10 +6,54 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do require Logger alias Pleroma.Config + alias Pleroma.Emoji.Pack @moduledoc "Detect new emojis by their shortcode and steals them" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + @pack_name "stolen" + + # Config defaults + @size_limit 50_000 + @download_unknown_size false + + defp create_pack() do + with {:ok, pack} = Pack.create(@pack_name) do + Pack.save_metadata( + %{ + "description" => "Collection of emoji auto-stolen from other instances", + "homepage" => Pleroma.Web.Endpoint.url(), + "can-download" => false, + "share-files" => false + }, + pack + ) + end + end + + defp load_or_create_pack() do + case Pack.load_pack(@pack_name) do + {:ok, pack} -> {:ok, pack} + {:error, :enoent} -> create_pack() + e -> e + end + end + + defp add_emoji(shortcode, extension, filedata) do + {:ok, pack} = load_or_create_pack() + # Make final path infeasible to predict to thwart certain kinds of attacks + # (48 bits is slighty more than 8 base62 chars, thus 9 chars) + salt = + :crypto.strong_rand_bytes(6) + |> :crypto.bytes_to_integer() + |> Base62.encode() + |> String.pad_leading(9, "0") + + filename = shortcode <> "-" <> salt <> "." <> extension + + Pack.add_file(pack, shortcode, filename, filedata) + end + defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], []) defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do @@ -20,30 +64,69 @@ defp shortcode_matches?(shortcode, pattern) do String.match?(shortcode, pattern) end - defp steal_emoji({shortcode, url}, emoji_dir_path) do + defp reject_emoji?({shortcode, _url}, installed_emoji) do + valid_shortcode? = String.match?(shortcode, ~r/^[a-zA-Z0-9_-]+$/) + + rejected_shortcode? = + [:mrf_steal_emoji, :rejected_shortcodes] + |> Config.get([]) + |> Enum.any?(fn pattern -> shortcode_matches?(shortcode, pattern) end) + + emoji_installed? = Enum.member?(installed_emoji, shortcode) + + !valid_shortcode? or rejected_shortcode? or emoji_installed? + end + + defp steal_emoji(%{} = response, {shortcode, extension}) do + case add_emoji(shortcode, extension, response.body) do + {:ok, _} -> + shortcode + + e -> + Logger.warning( + "MRF.StealEmojiPolicy: Failed to add #{shortcode} as #{extension}: #{inspect(e)}" + ) + + nil + end + end + + defp get_extension_if_safe(response) do + content_type = + :proplists.get_value("content-type", response.headers, MIME.from_path(response.url)) + + case content_type do + "image/" <> _ -> List.first(MIME.extensions(content_type)) + _ -> nil + end + end + + defp is_remote_size_within_limit?(url) do + with {:ok, %{status: status, headers: headers} = _response} when status in 200..299 <- + Pleroma.HTTP.request(:head, url, nil, [], []) do + content_length = :proplists.get_value("content-length", headers, nil) + size_limit = Config.get([:mrf_steal_emoji, :size_limit], @size_limit) + + accept_unknown = + Config.get([:mrf_steal_emoji, :download_unknown_size], @download_unknown_size) + + content_length <= size_limit or + (content_length == nil and accept_unknown) + else + _ -> false + end + end + + defp maybe_steal_emoji({shortcode, url}) do url = Pleroma.Web.MediaProxy.url(url) - with {:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do - size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000) + with {:remote_size, true} <- {:remote_size, is_remote_size_within_limit?(url)}, + {:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do + size_limit = Config.get([:mrf_steal_emoji, :size_limit], @size_limit) + extension = get_extension_if_safe(response) - if byte_size(response.body) <= size_limit do - extension = - url - |> URI.parse() - |> Map.get(:path) - |> Path.basename() - |> Path.extname() - - file_path = Path.join(emoji_dir_path, shortcode <> (extension || ".png")) - - case File.write(file_path, response.body) do - :ok -> - shortcode - - e -> - Logger.warning("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}") - nil - end + if byte_size(response.body) <= size_limit and extension do + steal_emoji(response, {shortcode, extension}) else Logger.debug( "MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{size_limit} B)" @@ -65,26 +148,10 @@ def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = messa if host != Pleroma.Web.Endpoint.host() and accept_host?(host) do installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) - emoji_dir_path = - Config.get( - [:mrf_steal_emoji, :path], - Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") - ) - - File.mkdir_p(emoji_dir_path) - new_emojis = foreign_emojis - |> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end) - |> Enum.filter(fn {shortcode, _url} -> - reject_emoji? = - [:mrf_steal_emoji, :rejected_shortcodes] - |> Config.get([]) - |> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end) - - !reject_emoji? - end) - |> Enum.map(&steal_emoji(&1, emoji_dir_path)) + |> Enum.reject(&reject_emoji?(&1, installed_emoji)) + |> Enum.map(&maybe_steal_emoji(&1)) |> Enum.filter(& &1) if !Enum.empty?(new_emojis) do diff --git a/lib/pleroma/web/activity_pub/object_validators/user_validator.ex b/lib/pleroma/web/activity_pub/object_validators/user_validator.ex new file mode 100644 index 000000000..90b5404f3 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/user_validator.ex @@ -0,0 +1,92 @@ +# Akkoma: Magically expressive social media +# Copyright © 2024 Akkoma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do + @moduledoc """ + Checks whether ActivityPub data represents a valid user + + Users don't go through the same ingest pipeline like activities or other objects. + To ensure this can only match a user and no users match in the other pipeline, + this is a separate from the generic ObjectValidator. + """ + + @behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating + + alias Pleroma.Object.Containment + alias Pleroma.Signature + + @impl true + def validate(object, meta) + + def validate(%{"type" => type, "id" => _id} = data, meta) + when type in ["Person", "Organization", "Group", "Application"] do + with :ok <- validate_pubkey(data), + :ok <- validate_inbox(data), + :ok <- contain_collection_origin(data) do + {:ok, data, meta} + else + {:error, e} -> {:error, e} + e -> {:error, e} + end + end + + def validate(_, _), do: {:error, "Not a user object"} + + defp mabye_validate_owner(nil, _actor), do: :ok + defp mabye_validate_owner(actor, actor), do: :ok + defp mabye_validate_owner(_owner, _actor), do: :error + + defp validate_pubkey( + %{"id" => id, "publicKey" => %{"id" => pk_id, "publicKeyPem" => _key}} = data + ) + when id != nil do + with {_, {:ok, kactor}} <- {:key, Signature.key_id_to_actor_id(pk_id)}, + true <- id == kactor, + :ok <- mabye_validate_owner(Map.get(data, "owner"), id) do + :ok + else + {:key, _} -> + {:error, "Unable to determine actor id from key id"} + + false -> + {:error, "Key id does not relate to user id"} + + _ -> + {:error, "Actor does not own its public key"} + end + end + + # pubkey is optional atm + defp validate_pubkey(_data), do: :ok + + defp validate_inbox(%{"id" => id, "inbox" => inbox}) do + case Containment.same_origin(id, inbox) do + :ok -> :ok + :error -> {:error, "Inbox on different doamin"} + end + end + + defp validate_inbox(_), do: {:error, "No inbox"} + + defp check_field_value(%{"id" => id} = _data, value) do + Containment.same_origin(id, value) + end + + defp maybe_check_field(data, field) do + with val when val != nil <- data[field], + :ok <- check_field_value(data, val) do + :ok + else + nil -> :ok + _ -> {:error, "#{field} on different domain"} + end + end + + defp contain_collection_origin(data) do + Enum.reduce(["followers", "following", "featured"], :ok, fn + field, :ok -> maybe_check_field(data, field) + _, error -> error + end) + end +end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 34617a218..963c6d8c6 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -109,7 +109,7 @@ def handle( %User{} = followed <- User.get_cached_by_ap_id(followed_user), {_, {:ok, _, _}, _, _} <- {:following, User.follow(follower, followed, :follow_pending), follower, followed} do - if followed.local && !followed.is_locked do + if followed.local && User.can_direct_follow_local(follower, followed) do {:ok, accept_data, _} = Builder.accept(followed, object) {:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true) end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 008aec475..f731b5286 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -16,10 +16,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.Endpoint - alias Pleroma.Web.Router.Helpers import Ecto.Query + use Pleroma.Web, :verified_routes + require Logger require Pleroma.Constants @@ -124,19 +125,15 @@ def make_date do end def generate_activity_id do - generate_id("activities") + url(~p[/activities/#{UUID.generate()}]) end def generate_context_id do - generate_id("contexts") + url(~p[/contexts/#{UUID.generate()}]) end def generate_object_id do - Helpers.o_status_url(Endpoint, :object, UUID.generate()) - end - - def generate_id(type) do - "#{Endpoint.url()}/#{type}/#{UUID.generate()}" + url(~p[/objects/#{UUID.generate()}]) end def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do @@ -154,7 +151,7 @@ def get_notified_from_object(object) do Notification.get_notified_from_activity(%Activity{data: object}, false) end - def maybe_create_context(context), do: context || generate_id("contexts") + def maybe_create_context(context), do: context || generate_context_id() @doc """ Enqueues an activity for federation if it's local diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 7333fb2c1..fe70022f1 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -12,24 +12,22 @@ defmodule Pleroma.Web.ActivityPub.UserView do alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Router.Helpers require Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Query def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do - %{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)} + %{"sharedInbox" => url(~p"/inbox")} end def render("endpoints.json", %{user: %User{local: true} = _user}) do %{ - "oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize), - "oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create), - "oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange), - "sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox), - "uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media) + "oauthAuthorizationEndpoint" => url(~p"/oauth/authorize"), + "oauthRegistrationEndpoint" => url(~p"/api/v1/apps"), + "oauthTokenEndpoint" => url(~p"/oauth/token"), + "sharedInbox" => url(~p"/inbox"), + "uploadMedia" => url(~p"/api/ap/upload_media") } end @@ -48,6 +46,7 @@ def render("service.json", %{user: user}) do "following" => "#{user.ap_id}/following", "followers" => "#{user.ap_id}/followers", "inbox" => "#{user.ap_id}/inbox", + "outbox" => "#{user.ap_id}/outbox", "name" => "Pleroma", "summary" => "An internal service actor for this Pleroma instance. No user-serviceable parts inside.", @@ -113,6 +112,8 @@ def render("user.json", %{user: user}) do } |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) + # Yes, the key is named ...Url eventhough it is a whole 'Image' object + |> Map.merge(maybe_insert_image("backgroundUrl", User.background_url(user))) |> Map.merge(Utils.make_json_ld_header()) end @@ -288,7 +289,12 @@ def collection(collection, iri, page, show_items \\ true, total \\ nil) do end defp maybe_make_image(func, key, user) do - if image = func.(user, no_default: true) do + image = func.(user, no_default: true) + maybe_insert_image(key, image) + end + + defp maybe_insert_image(key, image) do + if image do %{ key => %{ "type" => "Image", diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 1d7ac78a0..7344e1f77 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -17,9 +17,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.ModerationLogView - alias Pleroma.Web.Endpoint alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.Web.Router @users_page_size 50 @@ -256,7 +254,7 @@ def get_password_reset(conn, %{"nickname" => nickname}) do conn |> json(%{ token: token.token, - link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token) + link: url(~p[/api/v1/pleroma/password_reset/#{token.token}]) }) end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 2d14316d1..c15a246f2 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -451,6 +451,20 @@ def endorsements_operation do } end + def preferences_operation do + %Operation{ + tags: ["Account Preferences"], + description: "Preferences defined by the user in their account settings.", + summary: "Preferred common behaviors to be shared across clients.", + operationId: "AccountController.preferences", + security: [%{"oAuth" => ["read:accounts"]}], + responses: %{ + 200 => Operation.response("Preferences", "application/json", Account), + 401 => Operation.response("Error", "application/json", ApiError) + } + } + end + def identity_proofs_operation do %Operation{ tags: ["Retrieve account information"], @@ -709,6 +723,12 @@ defp update_credentials_request do description: "Number of days after which statuses will be deleted. Set to -1 to disable." }, + permit_followback: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: + "Whether follow requests from accounts the user is already following are auto-approved (when locked)." + }, accepts_direct_messages_from: %Schema{ type: :string, enum: [ @@ -740,6 +760,7 @@ defp update_credentials_request do discoverable: false, actor_type: "Person", status_ttl_days: 30, + permit_followback: true, accepts_direct_messages_from: "everybody" } } diff --git a/lib/pleroma/web/api_spec/operations/frontend_settings_operation.ex b/lib/pleroma/web/api_spec/operations/frontend_settings_operation.ex index 867a751b3..ede66709c 100644 --- a/lib/pleroma/web/api_spec/operations/frontend_settings_operation.ex +++ b/lib/pleroma/web/api_spec/operations/frontend_settings_operation.ex @@ -111,9 +111,9 @@ def available_frontends_operation() do def update_preferred_frontend_operation() do %Operation{ tags: ["Frontends"], - summary: "Frontend Settings Profiles", - description: "List frontend setting profiles", - operationId: "AkkomaAPI.FrontendSettingsController.available_frontends", + summary: "Update preferred frontend setting", + description: "Store preferred frontend in cookies", + operationId: "AkkomaAPI.FrontendSettingsController.update_preferred_frontend", requestBody: request_body( "Frontend", @@ -132,9 +132,11 @@ def update_preferred_frontend_operation() do responses: %{ 200 => Operation.response("Frontends", "application/json", %Schema{ - type: :array, - items: %Schema{ - type: :string + type: :object, + properties: %{ + frontend_name: %Schema{ + type: :string + } } }) } diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 9384acc32..c72fee197 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -137,7 +137,7 @@ defp instance do "background_upload_limit" => 4_000_000, "background_image" => "/static/image.png", "banner_upload_limit" => 4_000_000, - "description" => "Pleroma: An efficient and flexible fediverse server", + "description" => "Akkoma: The cooler fediverse server", "email" => "lain@lain.com", "languages" => ["en"], "max_toot_chars" => 5000, @@ -160,7 +160,7 @@ defp instance do "urls" => %{ "streaming_api" => "wss://lain.com" }, - "version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)" + "version" => "2.7.2 (compatible; Akkoma 3.9.3-232-g6fde75e1-develop)" } } end diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 2693eaceb..a922f6d1b 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -112,7 +112,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do akkoma: %Schema{ type: :object, properties: %{ - note_ttl_days: %Schema{type: :integer} + instance: %Schema{ + type: :object, + nullable: true, + properties: %{ + name: %Schema{type: :string}, + favicon: %Schema{type: :string, format: :uri, nullable: true}, + # XXX: proper nodeinfo schema + nodeinfo: %Schema{type: :object, nullable: true} + } + }, + status_ttl_days: %Schema{type: :integer, nullable: true}, + permit_followback: %Schema{type: :boolean} } }, source: %Schema{ @@ -205,6 +216,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do "pleroma-fe" => %{} } }, + "akkoma" => %{ + "instance" => %{ + "name" => "ihatebeinga.live", + "favicon" => "https://ihatebeinga.live/favicon.png", + "nodeinfo" => + %{ + # XXX: nodeinfo schema + } + }, + "status_ttl_days" => nil, + "permit_followback" => true + }, "source" => %{ "fields" => [], "note" => "foobar", diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index e3a251ca1..6628fcaf3 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -97,7 +97,11 @@ defmodule Pleroma.Web.Endpoint do Plug.Static, at: "/", from: :pleroma, - only: Pleroma.Constants.static_only_files(), + only: Pleroma.Web.static_paths(), + # JSON-LD is accepted by some servers for AP objects and activities, + # thus only enable it here instead of a global extension mapping + # (it's our only *.jsonld file anyway) + content_types: %{"litepub-0.1.jsonld" => "application/ld+json"}, # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength gzip: true, cache_control_for_etags: @static_cache_control, diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index dc3b1f94b..b320c9224 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -30,7 +30,7 @@ def feed_redirect(%{assigns: %{format: format}} = conn, _params) def feed_redirect(conn, %{"nickname" => nickname}) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do - redirect(conn, external: "#{Routes.user_feed_url(conn, :feed, user.nickname)}.atom") + redirect(conn, external: "#{url(~p"/users/#{user.nickname}/feed")}.atom") end end diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 7b6e01aad..b24f00620 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -34,9 +34,9 @@ def index(conn, _params) do index = if flavour == "fedibird-fe" do - "fedibird.index.html" + "fedibird.html" else - "glitchsoc.index.html" + "glitchsoc.html" end conn diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 26f46cc1a..2c7d6a893 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -51,7 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do plug( OAuthScopesPlug, %{scopes: ["read:accounts"]} - when action in [:verify_credentials, :endorsements, :identity_proofs] + when action in [:verify_credentials, :endorsements, :identity_proofs, :preferences] ) plug( @@ -222,6 +222,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language])) |> Maps.put_if_present(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value) |> Maps.put_if_present(:accepts_direct_messages_from, params[:accepts_direct_messages_from]) + |> Maps.put_if_present(:permit_followback, params[:permit_followback]) # What happens here: # @@ -544,4 +545,9 @@ def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, para @doc "GET /api/v1/identity_proofs" def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params) + + @doc "GET /api/v1/preferences" + def preferences(%{assigns: %{user: user}} = conn, _params) do + render(conn, "preferences.json", user: user) + end end diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index a9ccaa982..30e40ac42 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -54,12 +54,7 @@ def login(conn, params) do defp redirect_to_oauth_form(conn, _params) do with {:ok, app} <- local_mastofe_app() do path = - Routes.o_auth_path(conn, :authorize, - response_type: "code", - client_id: app.client_id, - redirect_uri: ".", - scope: Enum.join(app.scopes, " ") - ) + ~p[/oauth/authorize?#{[response_type: "code", client_id: app.client_id, redirect_uri: ".", scope: Enum.join(app.scopes, " ")]}] redirect(conn, to: path) end @@ -91,7 +86,7 @@ def password_reset(conn, params) do defp local_mastodon_post_login_path(conn) do case get_session(conn, :return_to) do nil -> - Routes.masto_fe_path(conn, :index, ["getting-started"]) + ~p"/web/getting-started" return_to -> delete_session(conn, :return_to) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index e3f91a4e3..e9c04e927 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -190,6 +190,17 @@ def render("instance.json", %{instance: %Pleroma.Instances.Instance{} = instance def render("instance.json", _), do: nil + def render("preferences.json", %{user: user} = _opts) do + # TODO: Do we expose more settings that make sense to plug in here? + %{ + "posting:default:visibility": user.default_scope, + "posting:default:sensitive": false, + "posting:default:language": nil, + "reading:expand:media": "default", + "reading:expand:spoilers": false + } + end + defp do_render("show.json", %{user: user} = opts) do user = User.sanitize_html(user, User.html_filter_policy(opts[:for])) display_name = user.name || user.nickname @@ -250,6 +261,9 @@ defp do_render("show.json", %{user: user} = opts) do |> MediaProxy.url() end + last_status_at = + if is_nil(user.last_status_at), do: nil, else: NaiveDateTime.to_date(user.last_status_at) + %{ id: to_string(user.id), username: username_from_nickname(user.nickname), @@ -278,10 +292,11 @@ defp do_render("show.json", %{user: user} = opts) do actor_type: user.actor_type } }, - last_status_at: user.last_status_at, + last_status_at: last_status_at, akkoma: %{ instance: render("instance.json", %{instance: instance}), - status_ttl_days: user.status_ttl_days + status_ttl_days: user.status_ttl_days, + permit_followback: user.permit_followback }, # Pleroma extensions # Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 47d1616c4..ac0955534 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -322,7 +322,7 @@ def render("show.json", %{activity: %{id: id, data: %{"object" => _object}} = ac url = if user.local do - Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) + url(~p[/notice/#{activity}]) else object.data["url"] || object.data["external_url"] || object.data["id"] end diff --git a/lib/pleroma/web/mastodon_api/views/tag_view.ex b/lib/pleroma/web/mastodon_api/views/tag_view.ex index 02108c736..6d3ea3c1a 100644 --- a/lib/pleroma/web/mastodon_api/views/tag_view.ex +++ b/lib/pleroma/web/mastodon_api/views/tag_view.ex @@ -1,7 +1,6 @@ defmodule Pleroma.Web.MastodonAPI.TagView do use Pleroma.Web, :view alias Pleroma.User - alias Pleroma.Web.Router.Helpers def render("index.json", %{tags: tags, for_user: user}) do render_many(tags, __MODULE__, "show.json", %{for_user: user}) @@ -17,7 +16,7 @@ def render("show.json", %{tag: tag, for_user: user}) do %{ name: tag.name, - url: Helpers.tag_feed_url(Pleroma.Web.Endpoint, :feed, tag.name), + url: url(~p[/tags/#{tag.name}]), history: [], following: following } diff --git a/lib/pleroma/web/media_proxy.ex b/lib/pleroma/web/media_proxy.ex index c5087c42c..19411d58e 100644 --- a/lib/pleroma/web/media_proxy.ex +++ b/lib/pleroma/web/media_proxy.ex @@ -14,6 +14,8 @@ defmodule Pleroma.Web.MediaProxy do @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + @mix_env Mix.env() + def cache_table, do: @cache_table @spec in_banned_urls(String.t()) :: boolean() @@ -144,8 +146,14 @@ def filename(url_or_path) do if path = URI.parse(url_or_path).path, do: Path.basename(path) end - def base_url do - Config.get([:media_proxy, :base_url], Endpoint.url()) + if @mix_env == :test do + def base_url do + Config.get([:media_proxy, :base_url], Endpoint.url()) + end + else + def base_url do + Config.get!([:media_proxy, :base_url]) + end end defp proxy_url(path, sig_base64, url_base64, filename) do diff --git a/lib/pleroma/web/metadata/providers/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex index d0ab5c19e..15f47b843 100644 --- a/lib/pleroma/web/metadata/providers/feed.ex +++ b/lib/pleroma/web/metadata/providers/feed.ex @@ -3,9 +3,9 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Metadata.Providers.Feed do - alias Pleroma.Web.Endpoint alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Router.Helpers + + use Pleroma.Web, :verified_routes @behaviour Provider @@ -16,7 +16,7 @@ def build_tags(%{user: user}) do [ rel: "alternate", type: "application/atom+xml", - href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" + href: ~p[/users/#{user.nickname}/feed.atom] ], []} ] end diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex index b2497d14e..ab48ea272 100644 --- a/lib/pleroma/web/metadata/providers/twitter_card.ex +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do alias Pleroma.Web.Metadata.Providers.Provider alias Pleroma.Web.Metadata.Utils + use Pleroma.Web, :verified_routes + @behaviour Provider @media_types ["image", "audio", "video"] @@ -112,7 +114,7 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do defp build_attachments(_id, _object), do: [] defp player_url(id) do - Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) + url(~p[/notice/#{id}/embed_player]) end # Videos have problems without dimensions, but we used to not provide WxH for images. diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index 277df1c46..29aa8c10e 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -39,6 +39,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do action_fallback(Pleroma.Web.OAuth.FallbackController) @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" + @state_cookie_name "akkoma_oauth_state" # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do @@ -443,13 +444,10 @@ def prepare_request(%Plug.Conn{} = conn, %{ |> Map.put("scope", scope) |> Jason.encode!() - params = - auth_attrs - |> Map.drop(~w(scope scopes client_id redirect_uri)) - |> Map.put("state", state) - # Handing the request to Ueberauth - redirect(conn, to: Routes.o_auth_path(conn, :request, provider, params)) + conn + |> put_resp_cookie(@state_cookie_name, state) + |> redirect(to: ~p"/oauth/#{provider}") end def request(%Plug.Conn{} = conn, params) do @@ -468,20 +466,26 @@ def request(%Plug.Conn{} = conn, params) do end def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do - params = callback_params(params) + params = callback_params(conn, params) messages = for e <- Map.get(failure, :errors, []), do: e.message message = Enum.join(messages, "; ") - conn - |> put_flash( - :error, - dgettext("errors", "Failed to authenticate: %{message}.", message: message) - ) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + error_message = dgettext("errors", "Failed to authenticate: %{message}.", message: message) + + if params["redirect_uri"] do + conn + |> put_flash( + :error, + error_message + ) + |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + else + send_resp(conn, :bad_request, error_message) + end end def callback(%Plug.Conn{} = conn, params) do - params = callback_params(params) + params = callback_params(conn, params) with {:ok, registration} <- Authenticator.get_registration(conn) do auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) @@ -511,8 +515,9 @@ def callback(%Plug.Conn{} = conn, params) do end end - defp callback_params(%{"state" => state} = params) do - Map.merge(params, Jason.decode!(state)) + defp callback_params(%Plug.Conn{} = conn, params) do + fetch_cookies(conn) + Map.merge(params, Jason.decode!(Map.get(conn.req_cookies, @state_cookie_name, "{}"))) end def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do @@ -623,7 +628,7 @@ def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested end # Special case: Local MastodonFE - defp redirect_uri(%Plug.Conn{} = conn, "."), do: Routes.auth_url(conn, :login) + defp redirect_uri(_, "."), do: url(~p"/web/login") defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex index 95a22895e..2b2872c9a 100644 --- a/lib/pleroma/web/o_status/o_status_controller.ex +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.Fallback.RedirectController alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.Plugs.RateLimiter - alias Pleroma.Web.Router plug( RateLimiter, @@ -87,7 +86,7 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do %{ activity_id: activity.id, object: object, - url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), + url: url(~p[/notice/#{activity.id}]), user: user } ) diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex index 488108b08..eb6a46736 100644 --- a/lib/pleroma/web/plugs/http_signature_plug.ex +++ b/lib/pleroma/web/plugs/http_signature_plug.ex @@ -5,8 +5,9 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do import Plug.Conn import Phoenix.Controller, only: [get_format: 1] + + use Pleroma.Web, :verified_routes alias Pleroma.Activity - alias Pleroma.Web.Router alias Pleroma.Signature alias Pleroma.Instances require Logger @@ -32,10 +33,10 @@ def call(conn, _opts) do end def route_aliases(%{path_info: ["objects", id], query_string: query_string}) do - ap_id = Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :object, id) + ap_id = url(~p[/objects/#{id}]) with %Activity{} = activity <- Activity.get_by_object_ap_id_with_object(ap_id) do - ["/notice/#{activity.id}", "/notice/#{activity.id}?#{query_string}"] + [~p"/notice/#{activity.id}", "/notice/#{activity.id}?#{query_string}"] else _ -> [] end diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex index 5f9a6ee83..b72b604a1 100644 --- a/lib/pleroma/web/plugs/instance_static.ex +++ b/lib/pleroma/web/plugs/instance_static.ex @@ -3,8 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Plugs.InstanceStatic do + import Plug.Conn + require Pleroma.Constants + alias Pleroma.Web.Plugs.Utils + @moduledoc """ This is a shim to call `Plug.Static` but with runtime `from` configuration. @@ -43,11 +47,25 @@ def call(conn, _) do conn end - defp call_static(conn, opts, from) do + defp set_static_content_type(conn, "/emoji/" <> _ = request_path) do + real_mime = MIME.from_path(request_path) + safe_mime = Utils.get_safe_mime_type(%{allowed_mime_types: ["image"]}, real_mime) + + put_resp_header(conn, "content-type", safe_mime) + end + + defp set_static_content_type(conn, request_path) do + put_resp_header(conn, "content-type", MIME.from_path(request_path)) + end + + defp call_static(%{request_path: request_path} = conn, opts, from) do opts = opts |> Map.put(:from, from) + |> Map.put(:set_content_type, false) - Plug.Static.call(conn, opts) + conn + |> set_static_content_type(request_path) + |> Pleroma.Web.Plugs.StaticNoCT.call(opts) end end diff --git a/lib/pleroma/web/plugs/static_no_content_type.ex b/lib/pleroma/web/plugs/static_no_content_type.ex new file mode 100644 index 000000000..ea00a2d5d --- /dev/null +++ b/lib/pleroma/web/plugs/static_no_content_type.ex @@ -0,0 +1,469 @@ +# This is almost identical to Plug.Static from Plug 1.15.3 (2024-01-16) +# It being copied is a temporary measure to fix an urgent bug without +# needing to wait for merge of a suitable patch upstream +# The differences are: +# - this leading comment +# - renaming of the module from 'Plug.Static' to 'Pleroma.Web.Plugs.StaticNoCT' +# - additon of set_content_type option + +defmodule Pleroma.Web.Plugs.StaticNoCT do + @moduledoc """ + A plug for serving static assets. + + It requires two options: + + * `:at` - the request path to reach for static assets. + It must be a string. + + * `:from` - the file system path to read static assets from. + It can be either: a string containing a file system path, an + atom representing the application name (where assets will + be served from `priv/static`), a tuple containing the + application name and the directory to serve assets from (besides + `priv/static`), or an MFA tuple. + + The preferred form is to use `:from` with an atom or tuple, since + it will make your application independent from the starting directory. + For example, if you pass: + + plug Plug.Static, from: "priv/app/path" + + Plug.Static will be unable to serve assets if you build releases + or if you change the current directory. Instead do: + + plug Plug.Static, from: {:app_name, "priv/app/path"} + + If a static asset cannot be found, `Plug.Static` simply forwards + the connection to the rest of the pipeline. + + ## Cache mechanisms + + `Plug.Static` uses etags for HTTP caching. This means browsers/clients + should cache assets on the first request and validate the cache on + following requests, not downloading the static asset once again if it + has not changed. The cache-control for etags is specified by the + `cache_control_for_etags` option and defaults to `"public"`. + + However, `Plug.Static` also supports direct cache control by using + versioned query strings. If the request query string starts with + "?vsn=", `Plug.Static` assumes the application is versioning assets + and does not set the `ETag` header, meaning the cache behaviour will + be specified solely by the `cache_control_for_vsn_requests` config, + which defaults to `"public, max-age=31536000"`. + + ## Options + + * `:encodings` - list of 2-ary tuples where first value is value of + the `Accept-Encoding` header and second is extension of the file to + be served if given encoding is accepted by client. Entries will be tested + in order in list, so entries higher in list will be preferred. Defaults + to: `[]`. + + In addition to setting this value directly it supports 2 additional + options for compatibility reasons: + + + `:brotli` - will append `{"br", ".br"}` to the encodings list. + + `:gzip` - will append `{"gzip", ".gz"}` to the encodings list. + + Additional options will be added in the above order (Brotli takes + preference over Gzip) to reflect older behaviour which was set due + to fact that Brotli in general provides better compression ratio than + Gzip. + + * `:cache_control_for_etags` - sets the cache header for requests + that use etags. Defaults to `"public"`. + + * `:etag_generation` - specify a `{module, function, args}` to be used + to generate an etag. The `path` of the resource will be passed to + the function, as well as the `args`. If this option is not supplied, + etags will be generated based off of file size and modification time. + Note it is [recommended for the etag value to be quoted](https://tools.ietf.org/html/rfc7232#section-2.3), + which Plug won't do automatically. + + * `:cache_control_for_vsn_requests` - sets the cache header for + requests starting with "?vsn=" in the query string. Defaults to + `"public, max-age=31536000"`. + + * `:only` - filters which requests to serve. This is useful to avoid + file system access on every request when this plug is mounted + at `"/"`. For example, if `only: ["images", "favicon.ico"]` is + specified, only files in the "images" directory and the + "favicon.ico" file will be served by `Plug.Static`. + Note that `Plug.Static` matches these filters against request + uri and not against the filesystem. When requesting + a file with name containing non-ascii or special characters, + you should use urlencoded form. For example, you should write + `only: ["file%20name"]` instead of `only: ["file name"]`. + Defaults to `nil` (no filtering). + + * `:only_matching` - a relaxed version of `:only` that will + serve any request as long as one of the given values matches the + given path. For example, `only_matching: ["images", "favicon"]` + will match any request that starts at "images" or "favicon", + be it "/images/foo.png", "/images-high/foo.png", "/favicon.ico" + or "/favicon-high.ico". Such matches are useful when serving + digested files at the root. Defaults to `nil` (no filtering). + + * `:headers` - other headers to be set when serving static assets. Specify either + an enum of key-value pairs or a `{module, function, args}` to return an enum. The + `conn` will be passed to the function, as well as the `args`. + + * `:content_types` - custom MIME type mapping. As a map with filename as key + and content type as value. For example: + `content_types: %{"apple-app-site-association" => "application/json"}`. + + * `:set_content_type` - by default Plug.Static (re)sets the content type header + using auto-detection and the `:content_types` map. But when set to `false` + no content-type header will be inserted instead retaining the original + value or lack thereof. This can be useful when custom logic for appropiate + content types is needed which cannot be reasonably expressed as a static + filename map. + + ## Examples + + This plug can be mounted in a `Plug.Builder` pipeline as follows: + + defmodule MyPlug do + use Plug.Builder + + plug Plug.Static, + at: "/public", + from: :my_app, + only: ~w(images robots.txt) + plug :not_found + + def not_found(conn, _) do + send_resp(conn, 404, "not found") + end + end + + """ + + @behaviour Plug + @allowed_methods ~w(GET HEAD) + + import Plug.Conn + alias Plug.Conn + + # In this module, the `:prim_file` Erlang module along with the `:file_info` + # record are used instead of the more common and Elixir-y `File` module and + # `File.Stat` struct, respectively. The reason behind this is performance: all + # the `File` operations pass through a single process in order to support node + # operations that we simply don't need when serving assets. + + require Record + Record.defrecordp(:file_info, Record.extract(:file_info, from_lib: "kernel/include/file.hrl")) + + defmodule InvalidPathError do + defexception message: "invalid path for static asset", plug_status: 400 + end + + @impl true + def init(opts) do + from = + case Keyword.fetch!(opts, :from) do + {_, _} = from -> from + {_, _, _} = from -> from + from when is_atom(from) -> {from, "priv/static"} + from when is_binary(from) -> from + _ -> raise ArgumentError, ":from must be an atom, a binary or a tuple" + end + + encodings = + opts + |> Keyword.get(:encodings, []) + |> maybe_add("br", ".br", Keyword.get(opts, :brotli, false)) + |> maybe_add("gzip", ".gz", Keyword.get(opts, :gzip, false)) + + %{ + encodings: encodings, + only_rules: {Keyword.get(opts, :only, []), Keyword.get(opts, :only_matching, [])}, + qs_cache: Keyword.get(opts, :cache_control_for_vsn_requests, "public, max-age=31536000"), + et_cache: Keyword.get(opts, :cache_control_for_etags, "public"), + et_generation: Keyword.get(opts, :etag_generation, nil), + headers: Keyword.get(opts, :headers, %{}), + content_types: Keyword.get(opts, :content_types, %{}), + set_content_type: Keyword.get(opts, :set_content_type, true), + from: from, + at: opts |> Keyword.fetch!(:at) |> Plug.Router.Utils.split() + } + end + + @impl true + def call( + conn = %Conn{method: meth}, + %{at: at, only_rules: only_rules, from: from, encodings: encodings} = options + ) + when meth in @allowed_methods do + segments = subset(at, conn.path_info) + + if allowed?(only_rules, segments) do + segments = Enum.map(segments, &uri_decode/1) + + if invalid_path?(segments) do + raise InvalidPathError, "invalid path for static asset: #{conn.request_path}" + end + + path = path(from, segments) + range = get_req_header(conn, "range") + encoding = file_encoding(conn, path, range, encodings) + serve_static(encoding, conn, segments, range, options) + else + conn + end + end + + def call(conn, _options) do + conn + end + + defp uri_decode(path) do + # TODO: Remove rescue as this can't fail from Elixir v1.13 + try do + URI.decode(path) + rescue + ArgumentError -> + raise InvalidPathError + end + end + + defp allowed?(_only_rules, []), do: false + defp allowed?({[], []}, _list), do: true + + defp allowed?({full, prefix}, [h | _]) do + h in full or (prefix != [] and match?({0, _}, :binary.match(h, prefix))) + end + + defp maybe_put_content_type(conn, false, _, _), do: conn + + defp maybe_put_content_type(conn, _, types, filename) do + content_type = Map.get(types, filename) || MIME.from_path(filename) + + conn + |> put_resp_header("content-type", content_type) + end + + defp serve_static({content_encoding, file_info, path}, conn, segments, range, options) do + %{ + qs_cache: qs_cache, + et_cache: et_cache, + et_generation: et_generation, + headers: headers, + content_types: types, + set_content_type: set_content_type + } = options + + case put_cache_header(conn, qs_cache, et_cache, et_generation, file_info, path) do + {:stale, conn} -> + filename = List.last(segments) + + conn + |> maybe_put_content_type(set_content_type, types, filename) + |> put_resp_header("accept-ranges", "bytes") + |> maybe_add_encoding(content_encoding) + |> merge_headers(headers) + |> serve_range(file_info, path, range, options) + + {:fresh, conn} -> + conn + |> maybe_add_vary(options) + |> send_resp(304, "") + |> halt() + end + end + + defp serve_static(:error, conn, _segments, _range, _options) do + conn + end + + defp serve_range(conn, file_info, path, [range], options) do + file_info(size: file_size) = file_info + + with %{"bytes" => bytes} <- Plug.Conn.Utils.params(range), + {range_start, range_end} <- start_and_end(bytes, file_size) do + send_range(conn, path, range_start, range_end, file_size, options) + else + _ -> send_entire_file(conn, path, options) + end + end + + defp serve_range(conn, _file_info, path, _range, options) do + send_entire_file(conn, path, options) + end + + defp start_and_end("-" <> rest, file_size) do + case Integer.parse(rest) do + {last, ""} when last > 0 and last <= file_size -> {file_size - last, file_size - 1} + _ -> :error + end + end + + defp start_and_end(range, file_size) do + case Integer.parse(range) do + {first, "-"} when first >= 0 -> + {first, file_size - 1} + + {first, "-" <> rest} when first >= 0 -> + case Integer.parse(rest) do + {last, ""} when last >= first -> {first, min(last, file_size - 1)} + _ -> :error + end + + _ -> + :error + end + end + + defp send_range(conn, path, 0, range_end, file_size, options) when range_end == file_size - 1 do + send_entire_file(conn, path, options) + end + + defp send_range(conn, path, range_start, range_end, file_size, _options) do + length = range_end - range_start + 1 + + conn + |> put_resp_header("content-range", "bytes #{range_start}-#{range_end}/#{file_size}") + |> send_file(206, path, range_start, length) + |> halt() + end + + defp send_entire_file(conn, path, options) do + conn + |> maybe_add_vary(options) + |> send_file(200, path) + |> halt() + end + + defp maybe_add_encoding(conn, nil), do: conn + defp maybe_add_encoding(conn, ce), do: put_resp_header(conn, "content-encoding", ce) + + defp maybe_add_vary(conn, %{encodings: encodings}) do + # If we serve gzip or brotli at any moment, we need to set the proper vary + # header regardless of whether we are serving gzip content right now. + # See: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/ + if encodings != [] do + update_in(conn.resp_headers, &[{"vary", "Accept-Encoding"} | &1]) + else + conn + end + end + + defp put_cache_header( + %Conn{query_string: "vsn=" <> _} = conn, + qs_cache, + _et_cache, + _et_generation, + _file_info, + _path + ) + when is_binary(qs_cache) do + {:stale, put_resp_header(conn, "cache-control", qs_cache)} + end + + defp put_cache_header(conn, _qs_cache, et_cache, et_generation, file_info, path) + when is_binary(et_cache) do + etag = etag_for_path(file_info, et_generation, path) + + conn = + conn + |> put_resp_header("cache-control", et_cache) + |> put_resp_header("etag", etag) + + if etag in get_req_header(conn, "if-none-match") do + {:fresh, conn} + else + {:stale, conn} + end + end + + defp put_cache_header(conn, _, _, _, _, _) do + {:stale, conn} + end + + defp etag_for_path(file_info, et_generation, path) do + case et_generation do + {module, function, args} -> + apply(module, function, [path | args]) + + nil -> + file_info(size: size, mtime: mtime) = file_info + < :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 diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index 300c33068..746203087 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do require Logger alias Pleroma.Web.MediaProxy + alias Pleroma.Web.Plugs.Utils @behaviour Plug # no slashes @@ -28,10 +29,21 @@ def init(_opts) do |> Keyword.put(:at, "/__unconfigured_media_plug") |> Plug.Static.init() - %{static_plug_opts: static_plug_opts} + config = Pleroma.Config.get(Pleroma.Upload) + allowed_mime_types = Keyword.fetch!(config, :allowed_mime_types) + uploader = Keyword.fetch!(config, :uploader) + + %{ + static_plug_opts: static_plug_opts, + allowed_mime_types: allowed_mime_types, + uploader: uploader + } end - def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do + def call( + %{request_path: <<"/", @path, "/", file::binary>>} = conn, + %{uploader: uploader} = opts + ) do conn = case fetch_query_params(conn) do %{query_params: %{"name" => name}} = conn -> @@ -44,10 +56,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do end |> merge_resp_headers([{"content-security-policy", "sandbox"}]) - config = Pleroma.Config.get(Pleroma.Upload) - - with uploader <- Keyword.fetch!(config, :uploader), - {:ok, get_method} <- uploader.get_file(file), + with {:ok, get_method} <- uploader.get_file(file), false <- media_is_banned(conn, get_method) do get_media(conn, get_method, opts) else @@ -68,13 +77,23 @@ defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url) defp media_is_banned(_, _), do: false + defp set_content_type(conn, opts, filepath) do + real_mime = MIME.from_path(filepath) + clean_mime = Utils.get_safe_mime_type(opts, real_mime) + put_resp_header(conn, "content-type", clean_mime) + end + defp get_media(conn, {:static_dir, directory}, opts) do static_opts = Map.get(opts, :static_plug_opts) |> Map.put(:at, [@path]) |> Map.put(:from, directory) + |> Map.put(:set_content_type, false) - conn = Plug.Static.call(conn, static_opts) + conn = + conn + |> set_content_type(opts, conn.request_path) + |> Pleroma.Web.Plugs.StaticNoCT.call(static_opts) if conn.halted do conn diff --git a/lib/pleroma/web/plugs/utils.ex b/lib/pleroma/web/plugs/utils.ex new file mode 100644 index 000000000..770a3eeb2 --- /dev/null +++ b/lib/pleroma/web/plugs/utils.ex @@ -0,0 +1,14 @@ +# Akkoma: Magically expressive social media +# Copyright © 2024 Akkoma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.Utils do + @moduledoc """ + Some helper functions shared across several plugs + """ + + def get_safe_mime_type(%{allowed_mime_types: allowed_mime_types} = _opts, mime) do + [maintype | _] = String.split(mime, "/", parts: 2) + if maintype in allowed_mime_types, do: mime, else: "application/octet-stream" + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6f4356fd7..ed71e5764 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -630,6 +630,8 @@ defmodule Pleroma.Web.Router do post("/tags/:id/follow", TagController, :follow) post("/tags/:id/unfollow", TagController, :unfollow) get("/followed_tags", TagController, :show_followed) + + get("/preferences", AccountController, :preferences) end scope "/api/web", Pleroma.Web do diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index 56ee4e41e..b1ea3178d 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Metadata - alias Pleroma.Web.Router.Helpers plug(:put_layout, :static_fe) plug(:assign_id) @@ -25,7 +24,13 @@ def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do true <- Visibility.is_public?(activity.object), {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)}, %User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do - meta = Metadata.build_tags(%{url: activity.data["id"], object: activity.object, user: user}) + meta = + Metadata.build_tags(%{ + activity_id: notice_id, + url: activity.data["id"], + object: activity.object, + user: user + }) timeline = activity.object.data["context"] @@ -111,11 +116,11 @@ def show(%{assigns: %{username_or_id: username_or_id, tab: tab}} = conn, params) end def show(%{assigns: %{object_id: _}} = conn, _params) do - url = Helpers.url(conn) <> conn.request_path + url = unverified_url(conn, conn.request_path) case Activity.get_create_by_object_ap_id_with_object(url) do %Activity{} = activity -> - to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity) + to = ~p[/notice/#{activity}] redirect(conn, to: to) _ -> @@ -124,11 +129,11 @@ def show(%{assigns: %{object_id: _}} = conn, _params) do end def show(%{assigns: %{activity_id: _}} = conn, _params) do - url = Helpers.url(conn) <> conn.request_path + url = unverified_url(conn, conn.request_path) case Activity.get_by_ap_id(url) do %Activity{} = activity -> - to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity) + to = ~p[/notice/#{activity}] redirect(conn, to: to) _ -> @@ -167,7 +172,7 @@ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do link = case user.local do - true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) + true -> ~p[/notice/#{activity}] _ -> data["url"] || data["external_url"] || data["id"] end diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex index f0c9ddd22..c1d83c5a0 100644 --- a/lib/pleroma/web/static_fe/static_fe_view.ex +++ b/lib/pleroma/web/static_fe/static_fe_view.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEView do alias Pleroma.Web.Gettext alias Pleroma.Web.MediaProxy alias Pleroma.Web.Metadata.Utils - alias Pleroma.Web.Router.Helpers use Phoenix.HTML diff --git a/lib/pleroma/web/telemetry.ex b/lib/pleroma/web/telemetry.ex index b03850600..269f9f238 100644 --- a/lib/pleroma/web/telemetry.ex +++ b/lib/pleroma/web/telemetry.ex @@ -101,7 +101,8 @@ defp distribution_metrics do ] end - defp summary_metrics do + # Summary metrics are currently not (yet) supported by the prometheus exporter + defp summary_metrics(byte_unit) do [ # Phoenix Metrics summary("phoenix.endpoint.stop.duration", @@ -118,10 +119,98 @@ defp summary_metrics do summary("pleroma.repo.query.idle_time", unit: {:native, :millisecond}), # VM Metrics - summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.memory.total", unit: {:byte, byte_unit}), summary("vm.total_run_queue_lengths.total"), summary("vm.total_run_queue_lengths.cpu"), - summary("vm.total_run_queue_lengths.io"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp sum_counter_pair(basename, opts) do + [ + sum(basename <> ".psum", opts), + counter(basename <> ".pcount", opts) + ] + end + + # Prometheus exporter doesn't support summaries, so provide fallbacks + defp summary_fallback_metrics(byte_unit \\ :byte) do + # Summary metrics are not supported by the Prometheus exporter + # https://github.com/beam-telemetry/telemetry_metrics_prometheus_core/issues/11 + # and sum metrics currently only work with integers + # https://github.com/beam-telemetry/telemetry_metrics_prometheus_core/issues/35 + # + # For VM metrics this is kindof ok as they appear to always be integers + # and we can use sum + counter to get the average between polls from their change + # But for repo query times we need to use a full distribution + + simple_buckets = [0, 1, 2, 4, 8, 16] + simple_buckets_quick = for t <- simple_buckets, do: t / 100.0 + + # Already included in distribution metrics anyway: + # phoenix.router_dispatch.stop.duration + # pleroma.repo.query.total_time + # pleroma.repo.query.queue_time + dist_metrics = + [ + distribution("phoenix.endpoint.stop.duration.fdist", + event_name: [:phoenix, :endpoint, :stop], + measurement: :duration, + unit: {:native, :millisecond}, + reporter_options: [ + buckets: simple_buckets + ] + ), + distribution("pleroma.repo.query.decode_time.fdist", + event_name: [:pleroma, :repo, :query], + measurement: :decode_time, + unit: {:native, :millisecond}, + reporter_options: [ + buckets: simple_buckets_quick + ] + ), + distribution("pleroma.repo.query.query_time.fdist", + event_name: [:pleroma, :repo, :query], + measurement: :query_time, + unit: {:native, :millisecond}, + reporter_options: [ + buckets: simple_buckets + ] + ), + distribution("pleroma.repo.query.idle_time.fdist", + event_name: [:pleroma, :repo, :query], + measurement: :idle_time, + unit: {:native, :millisecond}, + reporter_options: [ + buckets: simple_buckets + ] + ) + ] + + vm_metrics = + sum_counter_pair("vm.memory.total", + event_name: [:vm, :memory], + measurement: :total, + unit: {:byte, byte_unit} + ) ++ + sum_counter_pair("vm.total_run_queue_lengths.total", + event_name: [:vm, :total_run_queue_lengths], + measurement: :total + ) ++ + sum_counter_pair("vm.total_run_queue_lengths.cpu", + event_name: [:vm, :total_run_queue_lengths], + measurement: :cpu + ) ++ + sum_counter_pair("vm.total_run_queue_lengths.io.fsum", + event_name: [:vm, :total_run_queue_lengths], + measurement: :io + ) + + dist_metrics ++ vm_metrics + end + + defp common_metrics do + [ last_value("pleroma.local_users.total"), last_value("pleroma.domains.total"), last_value("pleroma.local_statuses.total"), @@ -129,8 +218,10 @@ defp summary_metrics do ] end - def prometheus_metrics, do: summary_metrics() ++ distribution_metrics() - def live_dashboard_metrics, do: summary_metrics() + def prometheus_metrics, + do: common_metrics() ++ distribution_metrics() ++ summary_fallback_metrics() + + def live_dashboard_metrics, do: common_metrics() ++ summary_metrics(:megabyte) defp periodic_measurements do [ diff --git a/lib/pleroma/web/templates/akkoma_api/frontend_switcher/switch.html.eex b/lib/pleroma/web/templates/akkoma_api/frontend_switcher/switch.html.eex index a0b0a2361..010a7fbad 100644 --- a/lib/pleroma/web/templates/akkoma_api/frontend_switcher/switch.html.eex +++ b/lib/pleroma/web/templates/akkoma_api/frontend_switcher/switch.html.eex @@ -2,7 +2,7 @@

    After you submit, you will need to refresh manually to get your new frontend!

    -<%= form_for @conn, Routes.frontend_switcher_path(@conn, :do_switch), fn f -> %> +<%= form_for @conn, ~p"/akkoma/frontend", fn f -> %> <%= select(f, :frontend, @choices) %> <%= submit do: "submit" %> diff --git a/lib/pleroma/web/templates/feed/feed/tag.atom.eex b/lib/pleroma/web/templates/feed/feed/tag.atom.eex index 6d497e84c..e85c08b2b 100644 --- a/lib/pleroma/web/templates/feed/feed/tag.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/tag.atom.eex @@ -9,13 +9,13 @@ xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/"> - <%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %> + <%= '#{url(~p"/tags/#{@tag}")}.rss' %> #<%= @tag %> <%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %> <%= feed_logo() %> <%= most_recent_update(@activities) %> - + " type="application/atom+xml"/> <%= for activity <- @activities do %> <%= render @view_module, "_tag_activity.atom", Map.merge(assigns, prepare_activity(activity, actor: true)) %> <% end %> diff --git a/lib/pleroma/web/templates/feed/feed/tag.rss.eex b/lib/pleroma/web/templates/feed/feed/tag.rss.eex index edcc3e436..7ee3ba5a3 100644 --- a/lib/pleroma/web/templates/feed/feed/tag.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/tag.rss.eex @@ -5,7 +5,7 @@ #<%= @tag %> <%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %> - <%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %> + <%= '#{url(~p"/tags/#{@tag}")}.rss' %> <%= feed_logo() %> 2b90d9 <%= for activity <- @activities do %> diff --git a/lib/pleroma/web/templates/feed/feed/user.atom.eex b/lib/pleroma/web/templates/feed/feed/user.atom.eex index 5c1f0ecbc..03585a9d5 100644 --- a/lib/pleroma/web/templates/feed/feed/user.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/user.atom.eex @@ -6,16 +6,16 @@ xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0"> - <%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %> + <%= url(~p"/users/#{@user.nickname}/feed") <> ".atom" %> <%= @user.nickname <> "'s timeline" %> <%= most_recent_update(@activities, @user) %> <%= logo(@user) %> - + " type="application/atom+xml"/> <%= render @view_module, "_author.atom", assigns %> <%= if last_activity(@activities) do %> - + " type="application/atom+xml"/> <% end %> <%= for activity <- @activities do %> diff --git a/lib/pleroma/web/templates/feed/feed/user.rss.eex b/lib/pleroma/web/templates/feed/feed/user.rss.eex index 6b842a085..f2eb7337e 100644 --- a/lib/pleroma/web/templates/feed/feed/user.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/user.rss.eex @@ -1,16 +1,16 @@ - <%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".rss" %> + <%= url(~p"/users/#{@user.nickname}/feed") <> ".rss" %> <%= @user.nickname <> "'s timeline" %> <%= most_recent_update(@activities, @user) %> <%= logo(@user) %> - <%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss' %> + <%= '#{url(~p"/users/#{@user.nickname}/feed")}.rss' %> <%= render @view_module, "_author.rss", assigns %> <%= if last_activity(@activities) do %> - <%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}' %> + <%= '#{url(~p"/users/#{@user.nickname}/feed")}.rss?max_id=#{last_activity(@activities).id}' %> <% end %> <%= for activity <- @activities do %> diff --git a/lib/pleroma/web/templates/masto_fe/fedibird.html.heex b/lib/pleroma/web/templates/masto_fe/fedibird.html.heex new file mode 100644 index 000000000..7070bd8d8 --- /dev/null +++ b/lib/pleroma/web/templates/masto_fe/fedibird.html.heex @@ -0,0 +1,58 @@ + + + + + + + + <%= Config.get([:instance, :name]) %> + + + + + + + + + + + + + + + + + + + + + + +
    + + diff --git a/lib/pleroma/web/templates/masto_fe/fedibird.index.html.eex b/lib/pleroma/web/templates/masto_fe/fedibird.index.html.eex deleted file mode 100644 index 6730c0ecc..000000000 --- a/lib/pleroma/web/templates/masto_fe/fedibird.index.html.eex +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - -<%= Config.get([:instance, :name]) %> - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - diff --git a/lib/pleroma/web/templates/masto_fe/glitchsoc.html.heex b/lib/pleroma/web/templates/masto_fe/glitchsoc.html.heex new file mode 100644 index 000000000..469c201a5 --- /dev/null +++ b/lib/pleroma/web/templates/masto_fe/glitchsoc.html.heex @@ -0,0 +1,57 @@ + + + + + + + + <%= Config.get([:instance, :name]) %> + + + + + + + + + + + + + + + + + + + + + + + + +
    + + diff --git a/lib/pleroma/web/templates/masto_fe/glitchsoc.index.html.eex b/lib/pleroma/web/templates/masto_fe/glitchsoc.index.html.eex deleted file mode 100644 index dadf8f413..000000000 --- a/lib/pleroma/web/templates/masto_fe/glitchsoc.index.html.eex +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - -<%= Config.get([:instance, :name]) %> - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - diff --git a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex index ee40cf277..b9b08c45d 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex @@ -1,15 +1,15 @@
    - <%= if get_flash(@conn, :info) do %> - + <%= if Flash.get(@flash, :info) do %> + <% end %> - <%= if get_flash(@conn, :error) do %> - + <%= if Flash.get(@flash, :error) do %> + <% end %>
    <%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %>
    - <%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %> + <%= form_for @conn, ~p"/oauth/mfa/verify", [as: "mfa"], fn f -> %>
    <%= label f, :code, Gettext.dpgettext("static_pages", "mfa recover recovery code prompt", "Recovery code") %> <%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %> @@ -21,7 +21,7 @@ <%= submit Gettext.dpgettext("static_pages", "mfa recover verify recovery code button", "Verify") %> <% end %> - "> + "> <%= Gettext.dpgettext("static_pages", "mfa recover use 2fa code link", "Enter a two-factor code") %> diff --git a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex index 734e62112..59827780b 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex @@ -1,15 +1,15 @@
    - <%= if get_flash(@conn, :info) do %> - + <%= if Flash.get(@flash, :info) do %> + <% end %> - <%= if get_flash(@conn, :error) do %> - + <%= if Flash.get(@flash, :error) do %> + <% end %>
    <%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %>
    - <%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %> + <%= form_for @conn, ~p"/oauth/mfa/verify", [as: "mfa"], fn f -> %>
    <%= label f, :code, Gettext.dpgettext("static_pages", "mfa auth code prompt", "Authentication code") %> <%= text_input f, :code, [autocomplete: "one-time-code", autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %> @@ -21,7 +21,7 @@ <%= submit Gettext.dpgettext("static_pages", "mfa auth verify code button", "Verify") %> <% end %> - "> + "> <%= Gettext.dpgettext("static_pages", "mfa auth page use recovery code link", "Enter a two-factor recovery code") %>
    diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex index 8b894cd58..97f2b770f 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -1,6 +1,6 @@

    <%= Gettext.dpgettext("static_pages", "oauth external provider page title", "Sign in with external provider") %>

    -<%= form_for @conn, Routes.o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %> +<%= form_for @conn, ~p"/oauth/prepare_request", [as: "authorization", method: "get"], fn f -> %>
    <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
    diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex index 1f661efb2..601b16b98 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex @@ -1,14 +1,14 @@ -<%= if get_flash(@conn, :info) do %> - +<%= if Flash.get(@flash, :info) do %> + <% end %> -<%= if get_flash(@conn, :error) do %> - +<%= if Flash.get(@flash, :error) do %> + <% end %>

    <%= Gettext.dpgettext("static_pages", "oauth register page title", "Registration Details") %>

    <%= Gettext.dpgettext("static_pages", "oauth register page fill form prompt", "If you'd like to register a new account, please provide the details below.") %>

    -<%= form_for @conn, Routes.o_auth_path(@conn, :register), [as: "authorization"], fn f -> %> +<%= form_for @conn, ~p"/oauth/register", [as: "authorization"], fn f -> %>
    <%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register page nickname prompt", "Nickname") %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 986e6ffce..420a17562 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -1,11 +1,11 @@ -<%= if get_flash(@conn, :info) do %> - +<%= if Flash.get(@flash, :info) do %> + <% end %> -<%= if get_flash(@conn, :error) do %> - +<%= if Flash.get(@flash, :error) do %> + <% end %> -<%= form_for @conn, Routes.o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> +<%= form_for @conn, ~p"/oauth/authorize", [as: "authorization"], fn f -> %> <%= if @user do %>
    -
    + "> diff --git a/lib/pleroma/web/templates/twitter_api/password/reset.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex index 6a544af51..390a8371f 100644 --- a/lib/pleroma/web/templates/twitter_api/password/reset.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex @@ -1,5 +1,5 @@

    Password Reset for <%= @user.nickname %>

    -<%= form_for @conn, Routes.reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %> +<%= form_for @conn, ~p"/api/v1/pleroma/password_reset", [as: "data"], fn f -> %>
    <%= label f, :password, Gettext.dpgettext("static_pages", "password reset form password prompt", "Password") %> <%= password_input f, :password %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex index e2d251fac..894b5c6ee 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex @@ -4,7 +4,7 @@

    <%= Gettext.dpgettext("static_pages", "remote follow header", "Remote follow") %>

    <%= @followee.nickname %>

    - <%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %> + <%= form_for @conn, ~p"/ostatus_subscribe", [as: "user"], fn f -> %> <%= hidden_input f, :id, value: @followee.id %> <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button", "Authorize") %> <% end %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex index 26340a906..b0084aac4 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex @@ -4,7 +4,7 @@

    <%= Gettext.dpgettext("static_pages", "remote follow header, need login", "Log in to follow") %>

    <%= @followee.nickname %>

    -<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %> +<%= form_for @conn, ~p"/ostatus_subscribe", [as: "authorization"], fn f -> %> <%= text_input f, :name, placeholder: Gettext.dpgettext("static_pages", "placeholder text for username entry", "Username"), required: true, autocomplete: "username" %>
    <%= password_input f, :password, placeholder: Gettext.dpgettext("static_pages", "placeholder text for password entry", "Password"), required: true, autocomplete: "password" %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex index 638212c1e..f34eba165 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex @@ -4,7 +4,7 @@

    <%= Gettext.dpgettext("static_pages", "remote follow mfa header", "Two-factor authentication") %>

    <%= @followee.nickname %>

    -<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %> +<%= form_for @conn, ~p"/ostatus_subscribe", [as: "mfa"], fn f -> %> <%= text_input f, :code, placeholder: Gettext.dpgettext("static_pages", "placeholder text for auth code entry", "Authentication code"), required: true %>
    <%= hidden_input f, :id, value: @followee.id %> diff --git a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex b/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex index d77174967..3105772e2 100644 --- a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex +++ b/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex @@ -2,7 +2,7 @@

    <%= Gettext.dpgettext("static_pages", "status interact error", "Error: %{error}", error: @error) %>

    <% else %>

    <%= raw Gettext.dpgettext("static_pages", "status interact header", "Interacting with %{nickname}'s %{status_link}", nickname: safe_to_string(html_escape(@nickname)), status_link: safe_to_string(link(Gettext.dpgettext("static_pages", "status interact header - status link text", "status"), to: @status_link))) %>

    - <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "status"], fn f -> %> + <%= form_for @conn, ~p"/main/ostatus", [as: "status"], fn f -> %> <%= hidden_input f, :status_id, value: @status_id %> <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %> <%= submit Gettext.dpgettext("static_pages", "status interact authorization button", "Interact") %> diff --git a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex b/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex index 848660f26..47b73f676 100644 --- a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex +++ b/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex @@ -2,7 +2,7 @@

    <%= Gettext.dpgettext("static_pages", "remote follow error", "Error: %{error}", error: @error) %>

    <% else %>

    <%= Gettext.dpgettext("static_pages", "remote follow header", "Remotely follow %{nickname}", nickname: @nickname) %>

    - <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %> + <%= form_for @conn, ~p"/main/ostatus", [as: "user"], fn f -> %> <%= hidden_input f, :nickname, value: @nickname %> <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %> <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for following with a remote account", "Follow") %> diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex index 42d7601ed..1927d2021 100644 --- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex @@ -38,7 +38,7 @@ def follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do defp follow_status(conn, _user, acct) do with {:ok, object} <- Fetcher.fetch_object_from_id(acct), %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object.data["id"]) do - redirect(conn, to: Routes.o_status_path(conn, :notice, activity_id)) + redirect(conn, to: ~p"/notice/#{activity_id}") else error -> handle_follow_error(conn, error) diff --git a/lib/pleroma/web/views/embed_view.ex b/lib/pleroma/web/views/embed_view.ex index 913d717be..fe1009e2f 100644 --- a/lib/pleroma/web/views/embed_view.ex +++ b/lib/pleroma/web/views/embed_view.ex @@ -13,7 +13,6 @@ defmodule Pleroma.Web.EmbedView do alias Pleroma.Web.Gettext alias Pleroma.Web.MediaProxy alias Pleroma.Web.Metadata.Utils - alias Pleroma.Web.Router.Helpers import Phoenix.HTML @@ -48,7 +47,7 @@ defp activity_content(%Activity{object: %Object{data: %{"content" => content}}}) defp activity_content(_), do: nil defp activity_url(%User{local: true}, activity) do - Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) + ~p[/notice/#{activity}] end defp activity_url(%User{local: false}, %Activity{object: %Object{data: data}}) do diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex index 305368c9d..dfb235976 100644 --- a/lib/pleroma/web/views/masto_fe_view.ex +++ b/lib/pleroma/web/views/masto_fe_view.ex @@ -85,7 +85,7 @@ def render("manifest.json", _params) do background_color: Config.get([:manifest, :background_color]), display: "standalone", scope: Pleroma.Web.Endpoint.url(), - start_url: Routes.masto_fe_path(Pleroma.Web.Endpoint, :index, ["getting-started"]), + start_url: ~p"/web/getting-started", categories: [ "social" ], diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex index ba8886548..e68341e20 100644 --- a/lib/pleroma/web/xml.ex +++ b/lib/pleroma/web/xml.ex @@ -26,13 +26,7 @@ def string_from_xpath(xpath, doc) do def parse_document(text) do try do - {doc, _rest} = - text - |> :binary.bin_to_list() - |> :xmerl_scan.string( - quiet: true, - allow_entities: false - ) + doc = SweetXml.parse(text, dtd: :none) {:ok, doc} rescue diff --git a/lib/pleroma/workers/cron/database_prune_worker.ex b/lib/pleroma/workers/cron/database_prune_worker.ex index 58995c69a..ced4b27b7 100644 --- a/lib/pleroma/workers/cron/database_prune_worker.ex +++ b/lib/pleroma/workers/cron/database_prune_worker.ex @@ -21,6 +21,9 @@ def perform(_job) do Logger.info("Pruning old undos") ActivityPruner.prune_undos() + Logger.info("Pruning old updates") + ActivityPruner.prune_updates() + Logger.info("Pruning old removes") ActivityPruner.prune_removes() diff --git a/mix.exs b/mix.exs index 235a3d65d..f501eec58 100644 --- a/mix.exs +++ b/mix.exs @@ -4,10 +4,10 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("3.10.4"), + version: version("3.12.0"), elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), - compilers: [:phoenix] ++ Mix.compilers(), + compilers: Mix.compilers(), elixirc_options: [warnings_as_errors: warnings_as_errors()], xref: [exclude: [:eldap]], start_permanent: Mix.env() == :prod, @@ -21,13 +21,13 @@ def project do source_url: "https://akkoma.dev/AkkomaGang/akkoma", docs: [ source_url_pattern: "https://akkoma.dev/AkkomaGang/akkoma/blob/develop/%{path}#L%{line}", - logo: "priv/static/images/logo.png", - extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"), + logo: "priv/static/logo-512.png", + extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/docs/**/*.md"), groups_for_extras: [ - "Installation manuals": Path.wildcard("docs/installation/*.md"), - Configuration: Path.wildcard("docs/config/*.md"), - Administration: Path.wildcard("docs/admin/*.md"), - "Pleroma's APIs and Mastodon API extensions": Path.wildcard("docs/api/*.md") + "Installation manuals": Path.wildcard("docs/docs/installation/*.md"), + Configuration: Path.wildcard("docs/docs/config/*.md"), + Administration: Path.wildcard("docs/docs/admin/*.md"), + "Pleroma's APIs and Mastodon API extensions": Path.wildcard("docs/docs/api/*.md") ], main: "readme", output: "priv/static/doc" @@ -114,7 +114,9 @@ defp oauth_deps do # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, "~> 1.6.15"}, + {:phoenix, "~> 1.7.0"}, + {:phoenix_view, "~> 2.0"}, + {:phoenix_live_dashboard, "~> 0.7.2"}, {:tzdata, "~> 1.1.1"}, {:plug_cowboy, "~> 2.6"}, {:phoenix_pubsub, "~> 2.1"}, @@ -146,7 +148,7 @@ defp deps do {:argon2_elixir, "~> 3.1"}, {:cors_plug, "~> 3.0"}, {:web_push_encryption, "~> 0.3.1"}, - {:swoosh, "~> 1.11"}, + {:swoosh, "~> 1.14.2"}, # for gmail adapter in swoosh {:mail, ">= 0.0.0"}, {:phoenix_swoosh, "~> 1.2"}, @@ -154,9 +156,11 @@ defp deps do {:ex_syslogger, "~> 2.0.0"}, {:floki, "~> 0.34"}, {:timex, "~> 3.7"}, - {:ueberauth, "~> 0.10"}, + {:ueberauth, "== 0.10.5"}, {:linkify, git: "https://akkoma.dev/AkkomaGang/linkify.git"}, - {:http_signatures, "~> 0.1.1"}, + {:http_signatures, + git: "https://akkoma.dev/AkkomaGang/http_signatures.git", + ref: "6640ce7d24c783ac2ef56e27d00d12e8dc85f396"}, {:telemetry, "~> 1.2"}, {:telemetry_poller, "~> 1.0"}, {:telemetry_metrics, "~> 0.6"}, @@ -175,22 +179,23 @@ defp deps do {:remote_ip, "~> 1.1.0"}, {:captcha, git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", - ref: "3bbfa8b5ea13accc1b1c40579a380d8e5cfd6ad2"}, + ref: "90f6ce7672f70f56708792a98d98bd05176c9176"}, {:restarter, path: "./restarter"}, - {:majic, "~> 1.0"}, + {:majic, + git: "https://akkoma.dev/AkkomaGang/majic.git", + ref: "80540b36939ec83f48e76c61e5000e0fd67706f0"}, {:eblurhash, "~> 1.2.2"}, {:open_api_spex, "~> 3.17"}, {:search_parser, git: "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git", ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"}, {:nimble_parsec, "~> 1.3", override: true}, - {:phoenix_live_dashboard, "~> 0.7.2"}, {:ecto_psql_extras, "~> 0.7"}, {:elasticsearch, git: "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", ref: "main"}, {:mfm_parser, git: "https://akkoma.dev/AkkomaGang/mfm-parser.git", - ref: "912fba81152d4d572e457fd5427f9875b2bc3dbe"}, + ref: "b21ab7754024af096f2d14247574f55f0063295b"}, ## dev & test {:ex_doc, "~> 0.30", only: :dev, runtime: false}, @@ -201,7 +206,9 @@ defp deps do {:mox, "~> 1.0", only: :test}, {:websockex, "~> 0.4.3", only: :test}, {:dialyxir, "~> 1.3", only: [:dev], runtime: false}, - {:mint, "~> 1.5.1", override: true} + {:elixir_xml_to_map, "~> 3.0", only: :test}, + {:mint, "~> 1.5.1", override: true}, + {:nimble_pool, "~> 1.0", override: true} ] ++ oauth_deps() end diff --git a/mix.lock b/mix.lock index 19b954d81..810356345 100644 --- a/mix.lock +++ b/mix.lock @@ -1,114 +1,117 @@ %{ - "argon2_elixir": {:hex, :argon2_elixir, "3.1.0", "4135e0a1b4ff800d42c85aa663e068efa3cb356297189b5b65caa992db8ec8cf", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "c08feae0ee0292165d1b945003363c7cd8523d002e0483c627dfca930291dd73"}, + "argon2_elixir": {:hex, :argon2_elixir, "3.2.1", "f47740bf9f2a39ffef79ba48eb25dea2ee37bcc7eadf91d49615591d1a6fce1a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "a813b78217394530b5fcf4c8070feee43df03ffef938d044019169c766315690"}, "base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"}, "bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"}, - "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, + "benchee": {:hex, :benchee, "1.2.0", "afd2f0caec06ce3a70d9c91c514c0b58114636db9d83c2dc6bfd416656618353", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "ee729e53217898b8fd30aaad3cce61973dab61574ae6f48229fe7ff42d5e4457"}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "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", "3bbfa8b5ea13accc1b1c40579a380d8e5cfd6ad2", [ref: "3bbfa8b5ea13accc1b1c40579a380d8e5cfd6ad2"]}, - "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]}, + "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, - "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"}, + "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, "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.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "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.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, - "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "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"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, - "earmark": {:hex, :earmark, "1.4.39", "acdb2f02c536471029dbcc509fbd6b94b89f40ad7729fb3f68f4b6944843f01d", [:mix], [{:earmark_parser, "~> 1.4.33", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "156c9d8ec3cbeccdbf26216d8247bdeeacc8c76b4d9eee7554be2f1b623ea440"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, + "dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"}, + "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "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_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.7.12", "e3bd8318702b069263d0118e7cdb6c66c5ff0a034f540f4c0158bd769e7dff6a", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "4a1d1d10b74ce033a428a99272038c90e444a0a1912a074e38a71ee9f667714c"}, - "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [: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", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"}, + "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.14", "7a20cfe913b0476542b43870e67386461258734896035e3f284039fd18bd4c4c", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "22f5f98592dd597db9416fcef00effae0787669fdcb6faf447e982b553798e98"}, + "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"}, "elasticsearch": {:git, "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", "6cd946f75f6ab9042521a009d1d32d29a90113ca", [ref: "main"]}, - "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, + "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, + "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "3.0.0", "67dcff30ecf72aed37ab08525133e4420717a749436e22bfece431e7dddeea7e", [:mix], [{:erlsom, "~> 1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "11222dd7f029f8db7a6662b41c992dbdb0e1c6e4fdea6a42056f9d27c847efbb"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, - "ex_aws": {:hex, :ex_aws, "2.4.4", "d7886eaca7e10f7bd3d9e9d2d5414cb336737b3ab2fddd4fa30358b725293fe0", [: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", "a7d63e485ca2b16fb804f3f20097827aa69885eea6e69fa75c98f353c9c91dc7"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [: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", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"}, + "ex_aws": {:hex, :ex_aws, "2.5.0", "1785e69350b16514c1049330537c7da10039b1a53e1d253bbd703b135174aec3", [: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", "971b86e5495fc0ae1c318e35e23f389e74cf322f2c02d34037c6fc6d405006f1"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.2", "cee302b8e9ee198cc0d89f1de2a7d6a8921e1a556574476cf5590d2156590fe3", [: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", "cc5bd945a22a99eece4721d734ae2452d3717e81c357a781c8574663254df4a1"}, "ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"}, - "ex_doc": {:hex, :ex_doc, "0.30.3", "bfca4d340e3b95f2eb26e72e4890da83e2b3a5c5b0e52607333bf5017284b063", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "fbc8702046c1d25edf79de376297e608ac78cdc3a29f075484773ad1718918b6"}, + "ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"}, "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_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.2.0", "6c5ef1be087a4ed613b0379c13f815c4d11742b36b67bb52cee7859847c84520", [: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", "064c4f23b4a6168f9187dac8984b056f2c531bb0787f559fd6a8b34b38aefbae"}, "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, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [: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", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, "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.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"}, + "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"}, "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.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.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.3.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", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "http_signatures": {:hex, :http_signatures, "0.1.1", "ca7ebc1b61542b163644c8c3b1f0e0f41037d35f2395940d3c6c7deceab41fd8", [:mix], [], "hexpm", "cc3b8a007322cc7b624c0c15eec49ee58ac977254ff529a3c482f681465942a3"}, + "http_signatures": {:git, "https://akkoma.dev/AkkomaGang/http_signatures.git", "6640ce7d24c783ac2ef56e27d00d12e8dc85f396", [ref: "6640ce7d24c783ac2ef56e27d00d12e8dc85f396"]}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"}, "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.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"}, "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, - "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, + "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, "linkify": {:git, "https://akkoma.dev/AkkomaGang/linkify.git", "2567e2c1073fa371fd26fd66dfa5bc77b6919c16", []}, - "mail": {:hex, :mail, "0.3.0", "f353ef5f41d9f2e483ba7c5df92cdfe27b990b2d8c8c41d84c7b2b40ec33989f", [:mix], [], "hexpm", "523d700b2231d887dc4ee41d3bb13f48358f6f118e55a67cef6d630d7907116c"}, - "majic": {:hex, :majic, "1.0.0", "37e50648db5f5c2ff0c9fb46454d034d11596c03683807b9fb3850676ffdaab3", [:make, :mix], [{:elixir_make, "~> 0.6.1", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7905858f76650d49695f14ea55cd9aaaee0c6654fa391671d4cf305c275a0a9e"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, + "majic": {:git, "https://akkoma.dev/AkkomaGang/majic.git", "80540b36939ec83f48e76c61e5000e0fd67706f0", [ref: "80540b36939ec83f48e76c61e5000e0fd67706f0"]}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, "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", "912fba81152d4d572e457fd5427f9875b2bc3dbe", [ref: "912fba81152d4d572e457fd5427f9875b2bc3dbe"]}, - "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, + "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "b21ab7754024af096f2d14247574f55f0063295b", [ref: "b21ab7754024af096f2d14247574f55f0063295b"]}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [: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", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, + "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"}, "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"}, - "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, - "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, - "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, - "oban": {:hex, :oban, "2.15.2", "8f934a49db39163633965139c8846d8e24c2beb4180f34a005c2c7c3f69a6aa2", [:mix], [{:ecto_sql, "~> 3.6", [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", "0f4a579ea48fc7489e0d84facf8b01566e142bdc6542d7dabce32c10e664f1e9"}, - "open_api_spex": {:hex, :open_api_spex, "3.17.3", "ada8e352eb786050dd639db2439d3316e92f3798eb2abd051f55bb9af825b37e", [: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", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "165db21a85ca83cffc8e7c8890f35b354eddda8255de7404a2848ed652b9f0fe"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"}, - "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, + "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "oban": {:hex, :oban, "2.15.4", "d49ab4ffb7153010e32f80fe9e56f592706238149ec579eb50f8a4e41d218856", [:mix], [{:ecto_sql, "~> 3.6", [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", "5fce611fdfffb13e9148df883116e5201adf1e731eb302cc88cde0588510079c"}, + "open_api_spex": {:hex, :open_api_spex, "3.18.0", "f9952b6bc8a1bf14168f3754981b7c8d72d015112bfedf2588471dd602e1e715", [: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", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "37849887ab67efab052376401fac28c0974b273ffaecd98f4532455ca0886464"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [: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.6", [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", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, + "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, "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_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [: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", [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", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"}, "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"}, - "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, - "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [: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", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, + "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [: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", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [: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", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, + "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "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.2", "a3ec9e3239d9b33f1e5841565c4eb200055c52cc0757a22b63ca2d529bbe764c", [: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", "80a918a9e9531d39f7bd70621422f3ebc93c01618c645f2d91306f50041ed90c"}, + "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [: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", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"}, "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.3", "739107b9050ea683c30e96de050bc59248fd27ec147696f79a8797ff9fa17153", [:mix, :rebar3], [], "hexpm", "6c6683f46fd4a1dfd98404b9f78dcabc7fcd8826613a89dcb984727a8c3099d7"}, + "recon": {:hex, :recon, "2.5.4", "05dd52a119ee4059fa9daa1ab7ce81bc7a8161a2f12e9d42e9d551ffd2ba901c", [:mix, :rebar3], [], "hexpm", "e9ab01ac7fc8572e41eb59385efeb3fb0ff5bf02103816535bacaedf327d0263"}, "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"}, "search_parser": {:git, "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git", "08971a81e68686f9ac465cfb6661d51c5e4e1e7f", [ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"}, "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.3", "debb256781c75ff6a8c5cbf7981146312b66f044a2898f453709a53e5031b45b", [:mix], [], "hexpm", "e110c867a1b3fe74bfc7dd9893aa851f0eed5518d0d7cad76d7baafd30e4f5ba"}, - "swoosh": {:hex, :swoosh, "1.11.4", "9b353f998cba3c5e101a0669559c2fb2757b5d9eb7db058bf08687d82e93e416", [: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_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d3390914022a456ae1604bfcb3431bd12509b2afe8c70296bae6c9dca4903d0f"}, + "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, + "swoosh": {:hex, :swoosh, "1.14.2", "cf686f92ad3b21e6651b20c50eeb1781f581dc7097ef6251b4d322a9f1d19339", [: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", "01d8fae72930a0b5c1bb9725df0408602ed8c5c3d59dc6e7a39c57b723cd1065"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, @@ -117,14 +120,16 @@ "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.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]}, - "tesla": {:hex, :tesla, "1.7.0", "a62dda2f80d4f8a925eb7b8c5b78c461e0eb996672719fe1a63b26321a5f8b4e", [: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.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [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", "2e64f01ebfdb026209b47bc651a0e65203fcff4ae79c11efb73c4852b00dc313"}, + "tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [: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", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"}, "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"}, "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.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, - "vex": {:hex, :vex, "0.9.0", "613ea5eb3055662e7178b83e25b2df0975f68c3d8bb67c1645f0573e1a78d606", [:mix], [], "hexpm", "c69fff44d5c8aa3f1faee71bba1dcab05dd36364c5a629df8bb11751240c857f"}, + "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, + "vex": {:hex, :vex, "0.9.1", "cb65348ebd1c4002861b65bef36e524c29d9a879c90119b2d0e674e323124277", [:mix], [], "hexpm", "a0f9f3959d127ad6a6a617c3f607ecfb1bc6f3c59f9c3614a901a46d1765bafe"}, "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.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [: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", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, } diff --git a/priv/gettext/pl/LC_MESSAGES/config_descriptions.po b/priv/gettext/pl/LC_MESSAGES/config_descriptions.po index d212a7d87..0692a61cd 100644 --- a/priv/gettext/pl/LC_MESSAGES/config_descriptions.po +++ b/priv/gettext/pl/LC_MESSAGES/config_descriptions.po @@ -8,50 +8,50 @@ ### to merge POT files into PO files. msgid "" msgstr "" -"PO-Revision-Date: 2023-08-04 14:26+0000\n" -"Last-Translator: Anonymous \n" +"PO-Revision-Date: 2023-10-22 11:53+0000\n" +"Last-Translator: subtype \n" "Language-Team: Polish \n" "Language: pl\n" "Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 4.18.2\n" -"Content-Transfer-Encoding: 8bit\n" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :logger" msgid "Logger-related settings" -msgstr "Logger-related settings" +msgstr "Ustawienia związane z Loggerem" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :mime" msgid "Mime Types settings" -msgstr "Mime Types settings" +msgstr "Ustawienia typów Mime" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma" msgid "Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)" msgstr "" -"Allows setting a token that can be used to authenticate requests with admin " -"privileges without a normal user account token. Append the `admin_token` " -"parameter to requests to utilize it. (Please reconsider using HTTP Basic " -"Auth or OAuth-based authentication if possible)" +"Pozwala na ustawienie tokenu, który może zostać użyty do uwierzytelniania " +"zapytań z uprawnieniami administratora bez zwykłego tokenu użytkownika. " +"Dodaj parametr `admin_token` by go użyć. (Proszę przemyśl użycie " +"uwierzytelnienia HTTP Basic Auth lub OAuth jeśli to możliwe)" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma" msgid "Authenticator" -msgstr "Authenticator" +msgstr "Uwierzytelniacz" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :cors_plug" msgid "CORS plug config" -msgstr "CORS plug config" +msgstr "Konfiguracja wtyczki CORS" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format, fuzzy @@ -60,1158 +60,1161 @@ msgid "Logger" msgstr "Logger" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :mime" msgid "Mime Types" -msgstr "Mime Types" +msgstr "Typy Mime" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma" msgid "Pleroma Admin Token" -msgstr "Pleroma Admin Token" +msgstr "Token Administratora Pleromy" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma" msgid "Pleroma Authenticator" -msgstr "Pleroma Authenticator" +msgstr "Uwierzytelniacz Pleromy" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:console" msgid "Console logger settings" -msgstr "Console logger settings" +msgstr "Ustawienia logowania do konsoli" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:ex_syslogger" msgid "ExSyslogger-related settings" -msgstr "ExSyslogger-related settings" +msgstr "Ustawienia zwiazane z ExSysloggerem" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub" msgid "ActivityPub-related settings" -msgstr "ActivityPub-related settings" +msgstr "Ustawienia związane z ActivityPub" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets" msgid "This section configures assets to be used with various frontends. Currently the only option relates to mascots on the mastodon frontend" msgstr "" -"This section configures assets to be used with various frontends. Currently " -"the only option relates to mascots on the mastodon frontend" +"Ta sekcja konfiguruje zasoby używane przez różne frontendy. Jak na razie, " +"jedyne opcje są związane z maskotkami na frontendzie Mastodon." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth" msgid "Authentication / authorization settings" -msgstr "Authentication / authorization settings" +msgstr "Ustawienia uwierzytelniania / autoryzacji." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications" msgid "Email notifications settings" -msgstr "Email notifications settings" +msgstr "Ustawienia powiadomień przez e-mail" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:features" msgid "Customizable features" -msgstr "Customizable features" +msgstr "Konfigurowalne funkcjonalności" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed" msgid "Configure feed rendering" -msgstr "Configure feed rendering" +msgstr "Konfiguruj wyświetlanie osi czasu" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends" msgid "Installed frontends management" -msgstr "Installed frontends management" +msgstr "Zarządzanie zainstalowanymi frontendami" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http" msgid "HTTP settings" -msgstr "HTTP settings" +msgstr "Ustawienia HTTP" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http_security" msgid "HTTP security settings" -msgstr "HTTP security settings" +msgstr "Ustawienia bezpieczeństwa HTTP" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance" msgid "Instance-related settings" -msgstr "Instance-related settings" +msgstr "Ustawienia związane z intancją" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instances_favicons" msgid "Control favicons for instances" -msgstr "Control favicons for instances" +msgstr "Kontrola favikonek dla instancji" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap" msgid "Use LDAP for user authentication. When a user logs in to the Pleroma instance, the name and password will be verified by trying to authenticate (bind) to a LDAP server. If a user exists in the LDAP directory but there is no account with the same name yet on the Pleroma instance then a new Pleroma account will be created with the same name as the LDAP user name." msgstr "" -"Use LDAP for user authentication. When a user logs in to the Pleroma " -"instance, the name and password will be verified by trying to authenticate " -"(bind) to a LDAP server. If a user exists in the LDAP directory but there is " -"no account with the same name yet on the Pleroma instance then a new Pleroma " -"account will be created with the same name as the LDAP user name." +"Użyj LDAP do uwierzytelnienia użytkowników. Kiedy użytkownik zaloguje się do " +"instancji Akkomy, nazwa i hasło zostaną zweryfikowane przez próbę " +"uwierzytelnienia w serwerze LDAP. Jeśli użytkownik istnieje w katalogu LDAP " +"ale nie ma jeszcze konta z tą nazwą na instancji Akkomy, wtedy nowe konto " +"Akkomy zostanie stworzone z tą samą nazwą jak nazwa użytkownika LDAP." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:majic_pool" msgid "Majic/libmagic configuration" -msgstr "Majic/libmagic configuration" +msgstr "Konfiguracja majic/libmagic" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:manifest" msgid "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE." msgstr "" -"This section describe PWA manifest instance-specific values. Currently this " -"option relate only for MastoFE." +"Ta sekcja opisuje właściwe dla instancji wartości manifestu PWA. Jak na " +"razie, ta opcja jest związana tylko z MastoFE." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_preview_proxy" msgid "Media preview proxy" -msgstr "Media preview proxy" +msgstr "Proxy podglądu mediów" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy" msgid "Media proxy" -msgstr "Media proxy" +msgstr "Proxy mediów" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:modules" msgid "Custom Runtime Modules" -msgstr "Custom Runtime Modules" +msgstr "Własne moduły czasu działania" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf" msgid "General MRF settings" -msgstr "General MRF settings" +msgstr "Ogólne ustawienia MRF" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_activity_expiration" msgid "Adds automatic expiration to all local activities" -msgstr "Adds automatic expiration to all local activities" +msgstr "Dodaj automatyczne wygaśnięcie do wszystkich lokalnych aktywności" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_follow_bot" msgid "Automatically follows newly discovered accounts." -msgstr "Automatically follows newly discovered accounts." +msgstr "Automatycznie śledzi nowoodnalezione konta" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hashtag" msgid "Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (without the leading #)\n\nNote: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.\n" msgstr "" -"Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (" -"without the leading #)\n" +"Odrzuć, usuń z całej znanej sieci, lub ustaw jako wrażliwe wiadomości z " +"określonymi hasztagami (bez #)\n" "\n" -"Note: This MRF Policy is always enabled, if you want to disable it you have " -"to set empty lists.\n" +"Uwaga: ta zasada MRF jest zawsze włączona. Jeśli chcesz ją wyłączyć, zostaw " +"listę pustą.\n" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hellthread" msgid "Block messages with excessive user mentions" -msgstr "Block messages with excessive user mentions" +msgstr "Blokuj wiadomości ze zbyt dużą liczbą wspomnień" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_keyword" msgid "Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html)." msgstr "" -"Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs" -".pm/elixir/Regex.html)." +"Odrzuć lub zmień treść wiadomości zawierających słowo kluczowe lub " +"pasujących do [regexa](https://hexdocs.pm/elixir/Regex.html)." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_mention" msgid "Block messages which mention a specific user" -msgstr "Block messages which mention a specific user" +msgstr "Blokuj wiadomości wspominające o określonych użytkownikach" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_normalize_markup" msgid "MRF NormalizeMarkup settings. Scrub configured hypertext markup." -msgstr "MRF NormalizeMarkup settings. Scrub configured hypertext markup." +msgstr "" +"Ustawienia MRF NormalizeMarkup. Wyczyść skonfigurowany markup hipertekstu." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_object_age" msgid "Rejects or delists posts based on their timestamp deviance from your server's clock." -msgstr "" -"Rejects or delists posts based on their timestamp deviance from your " -"server's clock." +msgstr "Odrzuć lub usuń z osi czasu wpisy biorąc pod uwagę ich datę utworzenia." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_rejectnonpublic" msgid "RejectNonPublic drops posts with non-public visibility settings." -msgstr "RejectNonPublic drops posts with non-public visibility settings." +msgstr "RejectNonPublic usuwa wpisy które nie są oznaczone jako publiczne." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple" msgid "Simple ingress policies" -msgstr "Simple ingress policies" +msgstr "Proste zasady modyfikacji wiadomości przychodzących" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_steal_emoji" msgid "Steals emojis from selected instances when it sees them." -msgstr "Steals emojis from selected instances when it sees them." +msgstr "Kradnij emoji z określonych instancji gdy tylko je zobaczysz." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_subchain" msgid "This policy processes messages through an alternate pipeline when a given message matches certain criteria. All criteria are configured as a map of regular expressions to lists of policy modules." msgstr "" -"This policy processes messages through an alternate pipeline when a given " -"message matches certain criteria. All criteria are configured as a map of " -"regular expressions to lists of policy modules." +"Ta taktyka przetwarza wiadomości przez alternatywny potok jeśli wiadomość " +"pasuje do określonych kryteriów. Wszystkie kryteria są skonfigurowane jako " +"mapa wyrażeń regularnych na listę modułów zasad." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_vocabulary" msgid "Filter messages which belong to certain activity vocabularies" -msgstr "Filter messages which belong to certain activity vocabularies" +msgstr "Filtruj wiadomości należących do określonego słownictwa aktywności" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:oauth2" msgid "Configure OAuth 2 provider capabilities" -msgstr "Configure OAuth 2 provider capabilities" +msgstr "Konfiguruj możliwości dostawcy OAuth 2" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:populate_hashtags_table" msgid "`populate_hashtags_table` background migration settings" -msgstr "`populate_hashtags_table` background migration settings" +msgstr "Ustawienia migracji w tle:`populate_hashtags_table`" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit" msgid "Rate limit settings. This is an advanced feature enabled only for :authentication by default." msgstr "" -"Rate limit settings. This is an advanced feature enabled only for :" -"authentication by default." +"Ustawienia limitów prędkości. To jest zaawansowana funkcjonalność włączona " +"domyślnie tylko dla :authentication." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rich_media" msgid "If enabled the instance will parse metadata from attached links to generate link previews" msgstr "" -"If enabled the instance will parse metadata from attached links to generate " -"link previews" +"Jeżeli włączone, ta instancja będzie przetwarzać metadane z dołączonych " +"linków by tworzyć podglądy" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:static_fe" msgid "Render profiles and posts using server-generated HTML that is viewable without using JavaScript" msgstr "" -"Render profiles and posts using server-generated HTML that is viewable " -"without using JavaScript" +"Wyświetlaj profile i posty używając tworzonego na serwerze HTML który można " +"przeglądać bez JavaScript" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:streamer" msgid "Settings for notifications streamer" -msgstr "Settings for notifications streamer" +msgstr "Ustawienia wysyłania powiadomień" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:uri_schemes" msgid "URI schemes related settings" -msgstr "URI schemes related settings" +msgstr "Ustawienia związane ze schematami URI" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:web_cache_ttl" msgid "The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration." msgstr "" -"The expiration time for the web responses cache. Values should be in " -"milliseconds or `nil` to disable expiration." +"Czas wygaśnięcia cache odpowiedzi sieciowych. Wartość powinna być podana w " +"milisekundach, lub `nil` by wyłączyć." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome" msgid "Welcome messages settings" -msgstr "Welcome messages settings" +msgstr "Ustawienia wiadomości powitalnych" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:workers" msgid "Includes custom worker options not interpretable directly by `Oban`" -msgstr "Includes custom worker options not interpretable directly by `Oban`" +msgstr "" +"Zawiera dodatkowe opcje workerów nieinterpretowalne bezpośrednio przez `Oban`" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-ConcurrentLimiter" msgid "Limits configuration for background tasks." -msgstr "Limits configuration for background tasks." +msgstr "Konfiguracja limitów dla zadań w tle." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban" msgid "[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration." msgstr "" -"[Oban](https://github.com/sorentwo/oban) asynchronous job processor " -"configuration." +"Konfiguracja procesora zadań asynchronicznych [Oban](https://github.com/" +"sorentwo/oban)." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha" msgid "Captcha-related settings" -msgstr "Captcha-related settings" +msgstr "Ustawienia związane z Captcha" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha.Kocaptcha" msgid "Kocaptcha is a very simple captcha service with a single API endpoint, the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint (https://captcha.kotobank.ch) is hosted by the developer." msgstr "" -"Kocaptcha is a very simple captcha service with a single API endpoint, the " -"source code is here: https://github.com/koto-bank/kocaptcha. The default " -"endpoint (https://captcha.kotobank.ch) is hosted by the developer." +"Kocaptcha to bardzo prosta usługa captcha z pojedynczym endpointem API, kod " +"źródłowy jest tu: https://github.com/koto-bank/kocaptcha. Domyślny endpoint " +"(https://captcha.kotobank.ch) jest hostowany przez developera." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer" msgid "Mailer-related settings" -msgstr "Mailer-related settings" +msgstr "Ustawienia związane z Mailerem" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.NewUsersDigestEmail" msgid "New users admin email digest" -msgstr "New users admin email digest" +msgstr "Administracyjne podsumowanie e-maili dla nowych użytkowników" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.UserEmail" msgid "Email template settings" -msgstr "Email template settings" +msgstr "Ustawienia szablonów e-maili" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter" msgid "Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs." msgstr "" -"Configuration for Pleroma's link formatter which parses mentions, hashtags, " -"and URLs." +"Konfiguracja formattera linków Akkomy, który przetwarza wspomnienia, " +"hasztagi, i URL." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.ScheduledActivity" msgid "Scheduled activities settings" -msgstr "Scheduled activities settings" +msgstr "Ustawienia zaplanowanych aktywności" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload" msgid "Upload general settings" -msgstr "Upload general settings" +msgstr "Ustawienia wysyłania plików" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename" msgid "Filter replaces the filename of the upload" -msgstr "Filter replaces the filename of the upload" +msgstr "Filtr zmienia nazwę wysyłanego pliku" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload.Filter.Mogrify" msgid "Uploads mogrify filter settings" -msgstr "Uploads mogrify filter settings" +msgstr "Ustawienia mogrifera wysyłanych plików" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.Local" msgid "Local uploader-related settings" -msgstr "Local uploader-related settings" +msgstr "Ustawnia związane z lokalnym wysyłaniem" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.S3" msgid "S3 uploader-related settings" -msgstr "S3 uploader-related settings" +msgstr "Ustawienia związane z wysyłaniem na S3" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format, fuzzy msgctxt "config description at :pleroma-Pleroma.User.Backup" msgid "Account Backup" -msgstr "Account Backup" +msgstr "Kopia zapasowa konta" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http" msgid "HTTP invalidate settings" -msgstr "HTTP invalidate settings" +msgstr "Ustawienia inwalidacji HTTP" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script" msgid "Invalidation script settings" -msgstr "Invalidation script settings" +msgstr "Ustawienia skryptu inwalidacyjnego" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Metadata" msgid "Metadata-related settings" -msgstr "Metadata-related settings" +msgstr "Ustawienia związane z metadanymi" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp" msgid "`Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.\n**If your instance is not behind at least one reverse proxy, you should not enable this plug.**\n" msgstr "" -"`Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git." -"pleroma.social/pleroma/remote_ip) but with runtime configuration.\n" -"**If your instance is not behind at least one reverse proxy, you should not " -"enable this plug.**\n" +"`Pleroma.Web.Plugs.RemoteIp` to podkładka by wywołać [`RemoteIp`](https://git" +".pleroma.social/pleroma/remote_ip) ale z konfiguracją w czasie działania.\n" +"**Jeśli twoja intancja nie jest przynajmniej za jednym odwrotnym proxy, nie " +"powinnoś włączać tej wtyczki.**\n" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Preload" msgid "Preload-related settings" -msgstr "Preload-related settings" +msgstr "Ustawienia preloadu" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Workers.PurgeExpiredActivity" msgid "Expired activities settings" -msgstr "Expired activities settings" +msgstr "Ustawienia wygasłych aktywności" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :web_push_encryption-:vapid_details" msgid "Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it." msgstr "" -"Web Push Notifications configuration. You can use the mix task mix " -"web_push.gen.keypair to generate it." +"Ustawienia powiadomień Push. Możesz użyć zadania mix web_push.gen.keypair by " +"je wygenerować." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :ex_aws-:s3" msgid "S3" msgstr "S3" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:console" msgid "Console Logger" -msgstr "Console Logger" +msgstr "Logger konsolowy" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:ex_syslogger" msgid "ExSyslogger" msgstr "ExSyslogger" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub" msgid "ActivityPub" msgstr "ActivityPub" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:assets" msgid "Assets" -msgstr "Assets" +msgstr "Zasoby" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:auth" msgid "Auth" -msgstr "Auth" +msgstr "Autoryzacja" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:email_notifications" msgid "Email notifications" -msgstr "Email notifications" +msgstr "Powiadomienia e-mail" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:emoji" msgid "Emoji" msgstr "Emoji" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:features" msgid "Features" -msgstr "Features" +msgstr "Funkcjonalności" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:feed" msgid "Feed" -msgstr "Feed" +msgstr "Oś czasu" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations" msgid "Frontend configurations" -msgstr "Frontend configurations" +msgstr "Konfiguracje frontenów" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends" msgid "Frontends" -msgstr "Frontends" +msgstr "Frontendy" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http" msgid "HTTP" msgstr "HTTP" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http_security" msgid "HTTP security" -msgstr "HTTP security" +msgstr "Bezpieczeństwo HTTP" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance" msgid "Instance" -msgstr "Instance" +msgstr "Instancja" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instances_favicons" msgid "Instances favicons" -msgstr "Instances favicons" +msgstr "Favikonki instancji" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap" msgid "LDAP" msgstr "LDAP" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:majic_pool" msgid "Majic pool" -msgstr "Majic pool" +msgstr "Pula Majic" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:manifest" msgid "Manifest" msgstr "Manifest" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:markup" msgid "Markup Settings" -msgstr "Markup Settings" +msgstr "Ustawienia Markup" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_preview_proxy" msgid "Media preview proxy" -msgstr "Media preview proxy" +msgstr "Proxy podglądu mediów" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy" msgid "Media proxy" -msgstr "Media proxy" +msgstr "Proxy mediów" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:modules" msgid "Modules" -msgstr "Modules" +msgstr "Moduły" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf" msgid "MRF" msgstr "MRF" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_activity_expiration" msgid "MRF Activity Expiration Policy" -msgstr "MRF Activity Expiration Policy" +msgstr "MRF Wygaśnięcie Aktyności" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_follow_bot" msgid "MRF FollowBot Policy" -msgstr "MRF FollowBot Policy" +msgstr "MRF Bot Śledzący" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hashtag" msgid "MRF Hashtag" msgstr "MRF Hashtag" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hellthread" msgid "MRF Hellthread" msgstr "MRF Hellthread" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_keyword" msgid "MRF Keyword" -msgstr "MRF Keyword" +msgstr "MRF Słowa Kluczowe" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_mention" msgid "MRF Mention" -msgstr "MRF Mention" +msgstr "MRF Wspomnienia" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_normalize_markup" msgid "MRF Normalize Markup" -msgstr "MRF Normalize Markup" +msgstr "MRF Normalizuj Markup" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_object_age" msgid "MRF Object Age" -msgstr "MRF Object Age" +msgstr "MRF Wiek Objektów" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_rejectnonpublic" msgid "MRF Reject Non Public" -msgstr "MRF Reject Non Public" +msgstr "MRF Odrzuć Niepubliczne" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple" msgid "MRF Simple" -msgstr "MRF Simple" +msgstr "MRF Prosty" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_steal_emoji" msgid "MRF Emojis" -msgstr "MRF Emojis" +msgstr "MRF Emoji" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_subchain" msgid "MRF Subchain" msgstr "MRF Subchain" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_vocabulary" msgid "MRF Vocabulary" -msgstr "MRF Vocabulary" +msgstr "MRF Słownictwo" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:oauth2" msgid "OAuth2" msgstr "OAuth2" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:populate_hashtags_table" msgid "Populate hashtags table" -msgstr "Populate hashtags table" +msgstr "Wypełnij tabelę hasztagów" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit" msgid "Rate limit" -msgstr "Rate limit" +msgstr "Limit prędkości" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated" msgid "Restrict Unauthenticated" -msgstr "Restrict Unauthenticated" +msgstr "Ogranicz nieuwieżytelnione" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rich_media" msgid "Rich media" -msgstr "Rich media" +msgstr "Bogate media" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:static_fe" msgid "Static FE" -msgstr "Static FE" +msgstr "Statyczny FE" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:streamer" msgid "Streamer" msgstr "Streamer" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:uri_schemes" msgid "URI Schemes" -msgstr "URI Schemes" +msgstr "Schematy URI" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:user" msgid "User" -msgstr "User" +msgstr "Użytkownik" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:web_cache_ttl" msgid "Web cache TTL" -msgstr "Web cache TTL" +msgstr "TTL cache sieciowego" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome" msgid "Welcome" -msgstr "Welcome" +msgstr "Witaj" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:workers" msgid "Workers" -msgstr "Workers" +msgstr "Workery" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-ConcurrentLimiter" msgid "ConcurrentLimiter" msgstr "ConcurrentLimiter" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban" msgid "Oban" msgstr "Oban" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Captcha" msgid "Pleroma.Captcha" msgstr "Pleroma.Captcha" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Captcha.Kocaptcha" msgid "Pleroma.Captcha.Kocaptcha" msgstr "Pleroma.Captcha.Kocaptcha" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer" msgid "Pleroma.Emails.Mailer" msgstr "Pleroma.Emails.Mailer" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.NewUsersDigestEmail" msgid "Pleroma.Emails.NewUsersDigestEmail" msgstr "Pleroma.Emails.NewUsersDigestEmail" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail" msgid "Pleroma.Emails.UserEmail" msgstr "Pleroma.Emails.UserEmail" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Formatter" msgid "Linkify" msgstr "Linkify" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.ScheduledActivity" msgid "Pleroma.ScheduledActivity" msgstr "Pleroma.ScheduledActivity" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload" msgid "Pleroma.Upload" msgstr "Pleroma.Upload" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename" msgid "Pleroma.Upload.Filter.AnonymizeFilename" msgstr "Pleroma.Upload.Filter.AnonymizeFilename" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload.Filter.Mogrify" msgid "Pleroma.Upload.Filter.Mogrify" msgstr "Pleroma.Upload.Filter.Mogrify" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Uploaders.Local" msgid "Pleroma.Uploaders.Local" msgstr "Pleroma.Uploaders.Local" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Uploaders.S3" msgid "Pleroma.Uploaders.S3" msgstr "Pleroma.Uploaders.S3" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.User" msgid "Pleroma.User" msgstr "Pleroma.User" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.User.Backup" msgid "Pleroma.User.Backup" msgstr "Pleroma.User.Backup" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.ApiSpec.CastAndValidate" msgid "Pleroma.Web.ApiSpec.CastAndValidate" msgstr "Pleroma.Web.ApiSpec.CastAndValidate" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http" msgid "Pleroma.Web.MediaProxy.Invalidation.Http" msgstr "Pleroma.Web.MediaProxy.Invalidation.Http" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script" msgid "Pleroma.Web.MediaProxy.Invalidation.Script" msgstr "Pleroma.Web.MediaProxy.Invalidation.Script" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Metadata" msgid "Pleroma.Web.Metadata" msgstr "Pleroma.Web.Metadata" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp" msgid "Pleroma.Web.Plugs.RemoteIp" msgstr "Pleroma.Web.Plugs.RemoteIp" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Preload" msgid "Pleroma.Web.Preload" msgstr "Pleroma.Web.Preload" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Workers.PurgeExpiredActivity" msgid "Pleroma.Workers.PurgeExpiredActivity" msgstr "Pleroma.Workers.PurgeExpiredActivity" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config label at :web_push_encryption-:vapid_details" msgid "Vapid Details" -msgstr "Vapid Details" +msgstr "Detale Vapid" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :ex_aws-:s3 > :access_key_id" msgid "S3 access key ID" -msgstr "S3 access key ID" +msgstr "ID klucza dostępu S3" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :ex_aws-:s3 > :host" msgid "S3 host" -msgstr "S3 host" +msgstr "Host S3" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :ex_aws-:s3 > :region" msgid "S3 region (for AWS)" -msgstr "S3 region (for AWS)" +msgstr "Region S3 (dla AWS)" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :ex_aws-:s3 > :secret_access_key" msgid "Secret access key" -msgstr "Secret access key" +msgstr "Sekretny klucz dostępu" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :logger > :backends" msgid "Where logs will be sent, :console - send logs to stdout, { ExSyslogger, :ex_syslogger } - to syslog, Quack.Logger - to Slack." msgstr "" -"Where logs will be sent, :console - send logs to stdout, { ExSyslogger, :" -"ex_syslogger } - to syslog, Quack.Logger - to Slack." +"Gdzie będą wysyłane logi, :console - wysyła na standardowe wyjście, { " +"ExSyslogger, :ex_syslogger } - do sysloga, Quack.Logger - na Slack." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:console > :format" msgid "Default: \"$date $time [$level] $levelpad$node $metadata $message\"" -msgstr "Default: \"$date $time [$level] $levelpad$node $metadata $message\"" +msgstr "Domyślne: \"$date $time [$level] $levelpad$node $metadata $message\"" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:console > :level" msgid "Log level" -msgstr "Log level" +msgstr "Poziom logowania" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:ex_syslogger > :format" msgid "Default: \"$date $time [$level] $levelpad$node $metadata $message\"" -msgstr "Default: \"$date $time [$level] $levelpad$node $metadata $message\"" +msgstr "Domyślne: \"$date $time [$level] $levelpad$node $metadata $message\"" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:ex_syslogger > :ident" msgid "A string that's prepended to every message, and is typically set to the app name" msgstr "" -"A string that's prepended to every message, and is typically set to the app " -"name" +"Tekst dopisywany do początku każdej wiadomości, i zazwyczaj ustawiany na " +"nazwę apki" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:ex_syslogger > :level" msgid "Log level" -msgstr "Log level" +msgstr "Poziom logowania" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma > :admin_token" msgid "Admin token" -msgstr "Admin token" +msgstr "Token admina" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :blockers_visible" msgid "Whether a user can see someone who has blocked them" -msgstr "Whether a user can see someone who has blocked them" +msgstr "Czy użytkownik może widzieć, kto go blokuje" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :follow_handshake_timeout" msgid "Following handshake timeout" -msgstr "Following handshake timeout" +msgstr "Limit czasu na hanshake prośby o śledzenie" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :note_replies_output_limit" msgid "The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)" msgstr "" -"The number of Note replies' URIs to be included with outgoing federation (`5`" -" to match Mastodon hardcoded value, `0` to disable the output)" +"Limit URI do odpowiedzi na notatkę zawartych w wychodzącej federacji (`5` by " +"dopasować się do wartości ustawionej na sztywno w Mastodonie, `0` by " +"wyłączyć)" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :outgoing_blocks" msgid "Whether to federate blocks to other instances" -msgstr "Whether to federate blocks to other instances" +msgstr "Czy informować inne instancje o blokadach" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :sign_object_fetches" msgid "Sign object fetches with HTTP signatures" -msgstr "Sign object fetches with HTTP signatures" +msgstr "Podpisuj zapytania o objekty podpisami HTTP" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :unfollow_blocked" msgid "Whether blocks result in people getting unfollowed" -msgstr "Whether blocks result in people getting unfollowed" +msgstr "Czy blokady usuwają śledzenie" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets > :default_mascot" msgid "This will be used as the default mascot on MastoFE. Default: `:pleroma_fox_tan`" -msgstr "" -"This will be used as the default mascot on MastoFE. Default: " -"`:pleroma_fox_tan`" +msgstr "Domyślna maskotka na MastoFE. Domyślna: `:pleroma_fox_tan`" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets > :default_user_avatar" msgid "URL of the default user avatar" -msgstr "URL of the default user avatar" +msgstr "URL domyślnego avatara nowych użytkowników" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets > :mascots" msgid "Keyword of mascots, each element must contain both an URL and a mime_type key" msgstr "" -"Keyword of mascots, each element must contain both an URL and a mime_type key" +"Słowa kluczowe maskotek, każdy element musi zawierać URL i klucz mime_type" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth > :auth_template" msgid "Authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.ee`." msgstr "" -"Authentication form template. By default it's `show.html` which corresponds " -"to `lib/pleroma/web/templates/o_auth/o_auth/show.html.ee`." +"Szablon formy uwierzytelnienia. Domyślnie jest to `show.html` które " +"odpowiada `lib/pleroma/web/templates/o_auth/o_auth/show.html.ee`." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth > :enforce_oauth_admin_scope_usage" msgid "OAuth admin scope requirement toggle. If enabled, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes). If disabled and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions." msgstr "" -"OAuth admin scope requirement toggle. If enabled, admin actions explicitly " -"demand admin OAuth scope(s) presence in OAuth token (client app must support " -"admin scopes). If disabled and token doesn't have admin scope(s), `is_admin` " -"user flag grants access to admin-specific actions." +"Zakres wymagań zakresu admina OAuth. Jeśli włączone, akcje admina wymagają " +"obecności zakresu admina OAuth w tokenie OAuth (aplikacja kliencka musi " +"wspierać zakresy admina). Jeśli wyłączone i token nie ma zakresów admina, " +"flaga użytkownika`is_admin` daje dostęp do zadań administracyjnych." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth > :oauth_consumer_strategies" msgid "The list of enabled OAuth consumer strategies. By default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. Each entry in this space-delimited string should be of format \"strategy\" or \"strategy:dependency\" (e.g. twitter or keycloak:ueberauth_keycloak_strategy in case dependency is named differently than ueberauth_)." msgstr "" -"The list of enabled OAuth consumer strategies. By default it's set by " -"OAUTH_CONSUMER_STRATEGIES environment variable. Each entry in this space-" -"delimited string should be of format \"strategy\" or \"strategy:dependency\" " -"(e.g. twitter or keycloak:ueberauth_keycloak_strategy in case dependency is " -"named differently than ueberauth_)." +"Lista włączonych strategii konsumenckich OAuth. Domyślnie jest to ustawione " +"przez zmienną środowiskową OAUTH_CONSUMER_STRATEGIES. Każdy fragment tego " +"ciągu znaków oddzielonego spacjamy powinien być formatu \"strategy\" or " +"\"strategy:dependency\" (np. twitter lub keycloak:" +"ueberauth_keycloak_strategy jeśli zależność jest nazwana inaczej niż " +"ueberatuh_)." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth > :oauth_consumer_template" msgid "OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`." msgstr "" -"OAuth consumer mode authentication form template. By default it's `consumer." -"html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer." -"html.eex`." +"Szablon formularza uwierzytelniania w trybie konsumenckim. Domyślnie jest " +"to`consumer.html` które odpowiada`lib/pleroma/web/templates/o_auth/o_auth/" +"consumer.html.eex`." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest" msgid "emails of \"what you've missed\" for users who have been inactive for a while" msgstr "" -"emails of \"what you've missed\" for users who have been inactive for a while" +"e-mail z \"co przegapiłoś\" dla użytkowników którzy nie logowali się przez " +"jakiś czas" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest > :active" msgid "Globally enable or disable digest emails" -msgstr "Globally enable or disable digest emails" +msgstr "Globalnie włącz lub wyłącz e-maile z podsumowaniem" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest > :inactivity_threshold" msgid "Minimum user inactivity threshold" -msgstr "Minimum user inactivity threshold" +msgstr "Próg minimalnej nieaktywności użytkowników" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest > :interval" msgid "Minimum interval between digest emails to one user" -msgstr "Minimum interval between digest emails to one user" +msgstr "" +"Minimalny okres między e-mailami z podsumowaniami dla jednego użytkownika" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest > :schedule" msgid "When to send digest email, in crontab format. \"0 0 0\" is the default, meaning \"once a week at midnight on Sunday morning\"." msgstr "" -"When to send digest email, in crontab format. \"0 0 0\" is the default, " -"meaning \"once a week at midnight on Sunday morning\"." +"Kiedy wysyłać e-mail z podsumowaniem, w formacie crontaba. Domyślne jest \"0 " +"0 0\", co znaczy \"raz na tydzień, o północy, w niedzielę wieczór\"." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:emoji > :default_manifest" msgid "Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays)." msgstr "" -"Location of the JSON-manifest. This manifest contains information about the " -"emoji-packs you can download. Currently only one manifest can be added (no " -"arrays)." +"Położenie manifestu JSON. Ten manifest zawiera informacje o paczkach emoji " +"które możesz ściągnąć. Obecnie można dodać tylko jeden manifest (bez tablic)." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:emoji > :groups" msgid "Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the group name and the value is the location or array of locations. * can be used as a wildcard." msgstr "" -"Emojis are ordered in groups (tags). This is an array of key-value pairs " -"where the key is the group name and the value is the location or array of " -"locations. * can be used as a wildcard." +"Emoji są podzielone na grupy (tagi). Jest to tablica par klucz-wartość, " +"gdzie klucz jest nazwą grupy, a wartość jest położeniem lub tablicą położeń. " +"* może być użyte by dopasować dowolny ciąg znaków." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:emoji > :pack_extensions" msgid "A list of file extensions for emojis, when no emoji.txt for a pack is present" -msgstr "" -"A list of file extensions for emojis, when no emoji.txt for a pack is present" +msgstr "Lista rozszerzeń plików emoji, jeśli nie ma emoji.txt dla paczki" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:emoji > :shortcode_globs" msgid "Location of custom emoji files. * can be used as a wildcard." -msgstr "Location of custom emoji files. * can be used as a wildcard." +msgstr "" +"Położenie własnych plików emoji. * może być użyte dla dopasowania dowolnego " +"ciągu znaków." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:features > :improved_hashtag_timeline" msgid "Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes)." msgstr "" -"Setting to force toggle / force disable improved hashtags timeline. " -"`:enabled` forces hashtags to be fetched from `hashtags` table for hashtags " -"timeline. `:disabled` forces object-embedded hashtags to be used (slower). " -"Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [" -"unless overridden] when HashtagsTableMigrator completes)." +"Ustawienie by przełączyć / wymusić wyłączenie poprawionej osi czasów " +"hasztagów. `:enabled` wymusza by hasztagi były pobierane z tabeli `hashtags` " +"dla osi czasu hasztagów. `:disabled` wymusza użycie hasztagów zagnieżdżonych " +"w objektach (wolniejsze). Zostaw jako `:auto` dla zachowania automatycznego (" +"jest automatycznie ustawione na `:enabled` [o ile nie nadpisane] kiedy " +"zakończy pracę HashtagsTableMigrator)." #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed > :post_title" msgid "Configure title rendering" -msgstr "Configure title rendering" +msgstr "Konfiguruj wyświetlanie kafli" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed > :post_title > :max_length" msgid "Maximum number of characters before truncating title" -msgstr "Maximum number of characters before truncating title" +msgstr "Maksymalna liczba znaków przed skróceniem tytułu" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed > :post_title > :omission" msgid "Replacement which will be used after truncating string" -msgstr "Replacement which will be used after truncating string" +msgstr "Czego użyć po skróceniu ciągu znaków" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe" msgid "Settings for Pleroma FE" -msgstr "Settings for Pleroma FE" +msgstr "Ustawienia Pleroma FE" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format, fuzzy diff --git a/priv/gettext/pl/LC_MESSAGES/posix_errors.po b/priv/gettext/pl/LC_MESSAGES/posix_errors.po index 3fe3a2db9..442b77324 100644 --- a/priv/gettext/pl/LC_MESSAGES/posix_errors.po +++ b/priv/gettext/pl/LC_MESSAGES/posix_errors.po @@ -8,146 +8,154 @@ ### to merge POT files into PO files. msgid "" msgstr "" +"PO-Revision-Date: 2023-10-09 18:53+0000\n" +"Last-Translator: subtype \n" +"Language-Team: Polish \n" "Language: pl\n" -"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10||n%100>=20) ? 1 : 2);\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.18.2\n" +"Content-Transfer-Encoding: 8bit\n" msgid "eperm" -msgstr "" +msgstr "Operacja niedozwolona" msgid "eacces" -msgstr "" +msgstr "Brak dostępu" msgid "eagain" -msgstr "" +msgstr "Zasoby chwilowo niedostępne" msgid "ebadf" -msgstr "" +msgstr "Błędny deskryptor pliku" msgid "ebadmsg" -msgstr "" +msgstr "Błędny komunikat" msgid "ebusy" -msgstr "" +msgstr "Urządzenie lub zasoby zajęte" msgid "edeadlk" -msgstr "" +msgstr "Uniknięto zakleszczenia zasobów" msgid "edeadlock" -msgstr "" +msgstr "Uniknięto zakleszczenia zasobów" msgid "edquot" -msgstr "" +msgstr "Przekroczony limit dyskowy" msgid "eexist" -msgstr "" +msgstr "Plik istnieje" msgid "efault" -msgstr "" +msgstr "Błędny adres" msgid "efbig" -msgstr "" +msgstr "Plik zbyt duży" msgid "eftype" -msgstr "" +msgstr "Niewłaściwy typ pliku" msgid "eintr" -msgstr "" +msgstr "Przerwane wywołanie systemowe" msgid "einval" -msgstr "" +msgstr "Zły argument" msgid "eio" -msgstr "" +msgstr "Błąd wejścia/wyjścia" msgid "eisdir" -msgstr "" +msgstr "Jest katalogiem" msgid "eloop" -msgstr "" +msgstr "Za duże zagnieżdżenie dowiązań symbolicznych" msgid "emfile" -msgstr "" +msgstr "Za dużo otwartych plików" msgid "emlink" -msgstr "" +msgstr "Za dużo dowiązań" msgid "emultihop" -msgstr "" +msgstr "Multihop attempted" msgid "enametoolong" -msgstr "" +msgstr "Za długa nazwa pliku" msgid "enfile" -msgstr "" +msgstr "Za dużo otwartych plików w systemie" msgid "enobufs" -msgstr "" +msgstr "Brak miejsca w buforze" msgid "enodev" -msgstr "" +msgstr "Nie ma takiego urządzenia" msgid "enolck" -msgstr "" +msgstr "Brak dostępnych blokad" msgid "enolink" -msgstr "" +msgstr "Połączenie zostało przerwane" msgid "enoent" -msgstr "" +msgstr "Nie ma takiego pliku ani katalogu" msgid "enomem" -msgstr "" +msgstr "Nie można przydzielić pamięci" msgid "enospc" -msgstr "" +msgstr "Brak miejsca na urządzeniu" msgid "enosr" -msgstr "" +msgstr "Brak dodatkowych strumieni" msgid "enostr" -msgstr "" +msgstr "Nie jest strumieniem" msgid "enosys" -msgstr "" +msgstr "Niezaimplementowana funkcja" msgid "enotblk" -msgstr "" +msgstr "Wymagane urządzenie blokowe" msgid "enotdir" -msgstr "" +msgstr "Nie jest katalogiem" msgid "enotsup" -msgstr "" +msgstr "Operacja nieobsługiwana" msgid "enxio" -msgstr "" +msgstr "Nie ma takiego urządzenia ani adresu" msgid "eopnotsupp" -msgstr "" +msgstr "Operacja na gnieździe nieobsługiwana" msgid "eoverflow" -msgstr "" +msgstr "Wartość za duża dla zdefiniowanego typu danych" msgid "epipe" -msgstr "" +msgstr "Przerwany potok" msgid "erange" -msgstr "" +msgstr "Za duży wynik" msgid "erofs" -msgstr "" +msgstr "System plików wyłącznie do odczytu" msgid "espipe" -msgstr "" +msgstr "Błędne przesunięcie" msgid "esrch" -msgstr "" +msgstr "Nie ma takiego procesu" msgid "estale" -msgstr "" +msgstr "Nieaktualny uchwyt pliku" msgid "etxtbsy" -msgstr "" +msgstr "Plik tekstowy zajęty" msgid "exdev" -msgstr "" +msgstr "Niepoprawne dowiązanie" diff --git a/priv/gettext/vi/LC_MESSAGES/errors.po b/priv/gettext/vi/LC_MESSAGES/errors.po index 445a5ae07..8aee75e9f 100644 --- a/priv/gettext/vi/LC_MESSAGES/errors.po +++ b/priv/gettext/vi/LC_MESSAGES/errors.po @@ -3,16 +3,16 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-09-06 11:13+0000\n" -"PO-Revision-Date: 2021-09-07 16:42+0000\n" -"Last-Translator: Hồ Nhất Duy \n" -"Language-Team: Vietnamese \n" +"PO-Revision-Date: 2023-09-08 05:53+0000\n" +"Last-Translator: Nguyễn Gia Phong \n" +"Language-Team: Vietnamese \n" "Language: vi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.6.2\n" +"X-Generator: Weblate 4.18.2\n" ## This file is a PO Template file. ## @@ -577,37 +577,37 @@ msgstr "Người này không phải quản trị viên." #, elixir-format msgid "Last export was less than a day ago" msgid_plural "Last export was less than %{days} days ago" -msgstr[0] "" +msgstr[0] "Vừa sao lưu lần cuối %{days} ngày trước" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:399 #, elixir-autogen, elixir-format msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters" -msgstr "" +msgstr "Quá dài: %{length} kí tự (tối đa %{limit})" #: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:33 #: lib/pleroma/web/plugs/user_is_staff_plug.ex:20 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgid "User is not a staff member." -msgstr "Người này không phải quản trị viên." +msgstr "Người dùng không phải quản trị viên." #: lib/pleroma/web/o_auth/o_auth_controller.ex:391 #, elixir-autogen, elixir-format msgid "Your account is awaiting approval." -msgstr "" +msgstr "Tài khoản của bạn đang chờ được duyệt." #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:256 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:259 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:262 #, elixir-autogen, elixir-format msgid "File is too large" -msgstr "" +msgstr "Tệp quá lớn" #: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:37 #: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:48 #: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:59 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgid "Hashtag not found" -msgstr "Không tìm thấy danh sách" +msgstr "Không tìm thấy hashtag" #: lib/pleroma/web/common_api/activity_draft.ex:144 #, elixir-autogen, elixir-format @@ -627,7 +627,7 @@ msgstr "" #: lib/pleroma/web/common_api/activity_draft.ex:126 #, elixir-autogen, elixir-format msgid "You can't quote a status that doesn't exist" -msgstr "" +msgstr "Bạn không thể trích dẫn bàn viết không tồn tại" #: lib/pleroma/web/embed_controller.ex:35 #, elixir-autogen, elixir-format @@ -637,9 +637,9 @@ msgstr "" #: lib/pleroma/web/embed_controller.ex:38 #, elixir-autogen, elixir-format msgid "Not authorized to view this post" -msgstr "" +msgstr "Không có quyền xem bài viết này" #: lib/pleroma/web/embed_controller.ex:32 -#, elixir-autogen, elixir-format, fuzzy +#, elixir-autogen, elixir-format msgid "Post not found" -msgstr "Không tìm thấy danh sách" +msgstr "Không tìm thấy bài viết" diff --git a/priv/repo/migrations/20240210000000_drop_chat_tables.exs b/priv/repo/migrations/20240210000000_drop_chat_tables.exs new file mode 100644 index 000000000..f83524e4d --- /dev/null +++ b/priv/repo/migrations/20240210000000_drop_chat_tables.exs @@ -0,0 +1,50 @@ +defmodule Pleroma.Repo.Migrations.DropChatTables do + use Ecto.Migration + + def up do + # Automatically drops associated indices and constraints + drop table(:chat_message_references) + drop table(:chats) + end + + def down do + # Ecto's default primary key is bigserial, thus configure manually + create table(:chats, primary_key: false) do + add(:id, :uuid, primary_key: true, autogenerated: true) + + add( + :user_id, + references(:users, type: :uuid, on_delete: :delete_all) + # yes, this was nullable + ) + + add( + :recipient, + references(:users, column: :ap_id, type: :string, on_delete: :delete_all) + # yes, this was nullable + ) + + timestamps() + end + + create(index(:chats, [:user_id, :recipient], unique: true)) + + create table(:chat_message_references, primary_key: false) do + add(:id, :uuid, primary_key: true, autogenerated: true) + add(:chat_id, references(:chats, type: :uuid, on_delete: :delete_all), null: false) + add(:object_id, references(:objects, on_delete: :delete_all), null: false) + add(:unread, :boolean, default: true, null: false) + timestamps() + end + + create(index(:chat_message_references, [:chat_id, "id desc"])) + create(unique_index(:chat_message_references, [:object_id, :chat_id])) + + create( + index(:chat_message_references, [:chat_id], + where: "unread = true", + name: "unread_messages_count_index" + ) + ) + end +end diff --git a/priv/repo/migrations/20240213120000_add_permit_followback.exs b/priv/repo/migrations/20240213120000_add_permit_followback.exs new file mode 100644 index 000000000..72475a58e --- /dev/null +++ b/priv/repo/migrations/20240213120000_add_permit_followback.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddPermitFollowback do + use Ecto.Migration + + def change do + alter table(:users) do + add(:permit_followback, :boolean, null: false, default: false) + end + end +end diff --git a/priv/repo/optional_migrations/20230422154018_drop_unused_indexes.exs b/priv/repo/optional_migrations/20230422154018_drop_unused_indexes.exs new file mode 100644 index 000000000..d8acb1034 --- /dev/null +++ b/priv/repo/optional_migrations/20230422154018_drop_unused_indexes.exs @@ -0,0 +1,74 @@ +defmodule Pleroma.Repo.Migrations.DropUnusedIndexes do + use Ecto.Migration + + @disable_ddl_transaction true + + @disable_migration_lock true + + def up do + drop_if_exists( + index(:activities, ["(data->>'actor')", "inserted_at desc"], name: :activities_actor_index) + ) + + drop_if_exists(index(:activities, ["(data->'to')"], name: :activities_to_index)) + + drop_if_exists(index(:activities, ["(data->'cc')"], name: :activities_cc_index)) + + drop_if_exists(index(:activities, ["(split_part(actor, '/', 3))"], name: :activities_hosts)) + + drop_if_exists( + index(:activities, ["(data->'object'->>'inReplyTo')"], name: :activities_in_reply_to) + ) + + drop_if_exists( + index(:activities, ["((data #> '{\"object\",\"likes\"}'))"], name: :activities_likes) + ) + end + + def down do + create_if_not_exists( + index(:activities, ["(data->>'actor')", "inserted_at desc"], + name: :activities_actor_index, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["(data->'to')"], + name: :activities_to_index, + using: :gin, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["(data->'cc')"], + name: :activities_cc_index, + using: :gin, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["(split_part(actor, '/', 3))"], + name: :activities_hosts, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["(data->'object'->>'inReplyTo')"], + name: :activities_in_reply_to, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["((data #> '{\"object\",\"likes\"}'))"], + name: :activities_likes, + using: :gin, + concurrently: true + ) + ) + end +end diff --git a/priv/repo/optional_migrations/pleroma_develop_rollbacks/20230306112859_instances_add_metadata.exs b/priv/repo/optional_migrations/pleroma_develop_rollbacks/20230306112859_instances_add_metadata.exs new file mode 100644 index 000000000..ab8ac83a0 --- /dev/null +++ b/priv/repo/optional_migrations/pleroma_develop_rollbacks/20230306112859_instances_add_metadata.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.InstancesAddMetadata do + use Ecto.Migration + + def down do + alter table(:instances) do + remove_if_exists(:metadata, :map) + end + end + + def up do + alter table(:instances) do + add_if_not_exists(:metadata, :map) + end + end +end diff --git a/priv/repo/optional_migrations/pleroma_develop_rollbacks/20230504173400_remove_user_ap_enabled.exs b/priv/repo/optional_migrations/pleroma_develop_rollbacks/20230504173400_remove_user_ap_enabled.exs new file mode 100644 index 000000000..f399d9fd7 --- /dev/null +++ b/priv/repo/optional_migrations/pleroma_develop_rollbacks/20230504173400_remove_user_ap_enabled.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.RemoveUserApEnabled do + use Ecto.Migration + + def up do + alter table(:users) do + remove_if_exists(:ap_enabled, :boolean) + end + end + + def down do + alter table(:users) do + add_if_not_exists(:ap_enabled, :boolean, default: true, null: false) + end + end +end diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 37ffaef3a..74de910fd 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -26,7 +26,9 @@ defmodule Pleroma.HTML.Scrubber.Default do "nofollow", "noopener", "noreferrer", - "ugc" + "ugc", + "tag ugc", + "ugc tag" ]) Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) @@ -126,6 +128,4 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes(:small, []) Meta.strip_everything_not_covered() - - defp scrub_css(value), do: value end diff --git a/priv/scrubbers/links_only.ex b/priv/scrubbers/links_only.ex index b30a00589..5ec35c027 100644 --- a/priv/scrubbers/links_only.ex +++ b/priv/scrubbers/links_only.ex @@ -19,7 +19,9 @@ defmodule Pleroma.HTML.Scrubber.LinksOnly do "noopener", "noreferrer", "me", - "ugc" + "ugc", + "tag ugc", + "ugc tag" ]) Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) diff --git a/priv/scrubbers/twitter_text.ex b/priv/scrubbers/twitter_text.ex index c4e796cad..9cb455a27 100644 --- a/priv/scrubbers/twitter_text.ex +++ b/priv/scrubbers/twitter_text.ex @@ -26,7 +26,9 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do "tag", "nofollow", "noopener", - "noreferrer" + "noreferrer", + "tag ugc", + "ugc tag" ]) Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) diff --git a/priv/templates/sample_config.eex b/priv/templates/sample_config.eex index 0068969ac..724c5a9d5 100644 --- a/priv/templates/sample_config.eex +++ b/priv/templates/sample_config.eex @@ -78,6 +78,8 @@ config :joken, default_signer: "<%= jwt_secret %>" config :pleroma, configurable_from_database: <%= db_configurable? %> +config :pleroma, Pleroma.Upload, <%= if Kernel.length(upload_filters) > 0 do -"config :pleroma, Pleroma.Upload, filters: #{inspect(upload_filters)}" +" filters: #{inspect(upload_filters)}," end %> + base_url: "<%= media_url %>" diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index f05e3cd57..42fa06a53 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -2,21 +2,28 @@ # XXX: This should be removed when elixir's releases get custom command support detect_flavour() { - arch="amd64" - if getconf GNU_LIBC_VERSION >/dev/null; then - libc_postfix="" - elif [ "$(ldd 2>&1 | head -c 9)" = "musl libc" ]; then - libc_postfix="-musl" - elif [ "$(find /lib/libc.musl* | wc -l)" ]; then - libc_postfix="-musl" - else - echo "Unsupported libc" >&2 - exit 1 + machine_type=$(uname -m) + if [ ${machine_type} = 'aarch64' ] ; then + arch='arm64' + elif [ ${machine_type} = 'x86_64' ] ; then + arch='amd64' + if getconf GNU_LIBC_VERSION >/dev/null; then + libc_postfix="" + elif [ "$(ldd 2>&1 | head -c 9)" = "musl libc" ]; then + libc_postfix="-musl" + elif [ "$(find /lib/libc.musl* | wc -l)" ]; then + libc_postfix="-musl" + else + echo "Unsupported libc" >&2 + exit 1 + fi + else + echo "Could not detect your architecture please pass them via --flavour" + exit 1 fi - echo "$arch$libc_postfix" + echo "$arch$libc_postfix" } - detect_branch() { version="$(cut -d' ' -f2 <"$RELEASE_ROOT"/releases/start_erl.data)" # Expected format: major.minor.patch_version(-number_of_commits_ahead_of_tag-gcommit_hash).branch diff --git a/rel/vm.args.eex b/rel/vm.args.eex index 71e803264..6c252186e 100644 --- a/rel/vm.args.eex +++ b/rel/vm.args.eex @@ -9,3 +9,9 @@ ## Tweak GC to run more often ##-env ERL_FULLSWEEP_AFTER 10 + +## Disable busy waits; vastly reduces CPU usage while idle +## See https://docs.akkoma.dev/stable/configuration/optimisation/optimizing_beam/#virtual-machine-andor-few-cpu-cores ++sbwt none ++sbwtdcpu none ++sbwtdio none diff --git a/test/fixtures/bridgy/actor.json b/test/fixtures/bridgy/actor.json index 5b2d8982b..b4e859a82 100644 --- a/test/fixtures/bridgy/actor.json +++ b/test/fixtures/bridgy/actor.json @@ -70,7 +70,7 @@ "preferredUsername": "jk.nipponalba.scot", "summary": "", "publicKey": { - "id": "jk.nipponalba.scot", + "id": "https://fed.brid.gy/jk.nipponalba.scot#key", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdarxwzxnNbJ2hneWOYHkYJowk\npyigQtxlUd0VjgSQHwxU9kWqfbrHBVADyTtcqi/4dAzQd3UnCI1TPNnn4LPZY9PW\noiWd3Zl1/EfLFxO7LU9GS7fcSLQkyj5JNhSlN3I8QPudZbybrgRDVZYooDe1D+52\n5KLGqC2ajrIVOiDRTQIDAQAB\n-----END PUBLIC KEY-----" }, "inbox": "https://fed.brid.gy/jk.nipponalba.scot/inbox", diff --git a/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json b/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json index ca76d6e17..70d750e5d 100644 --- a/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json +++ b/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json @@ -37,7 +37,7 @@ "sharedInbox": "https://osada.macgirvin.com/inbox" }, "publicKey": { - "id": "https://osada.macgirvin.com/channel/mike/public_key_pem", + "id": "https://osada.macgirvin.com/channel/mike", "owner": "https://osada.macgirvin.com/channel/mike", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAskSyK2VwBNKbzZl9XNJk\nvxU5AAilmRArMmmKSzphdHaVBHakeafUfixvqNrQ/oX2srJvJKcghNmEMrJ6MJ7r\npeEndVOo7pcP4PwVjtnC06p3J711q5tBehqM25BfCLCrB2YqWF6c8zk3CPN3Na21\n8k5s4cO95N/rGN+Po0XFAX/HjKjlpgNpKRDrpxmXxTU8NZfAqeQGJ5oiMBZI9vVB\n+eU7t1L6F5/XWuUCeP4OMrG8oZX822AREba8rknS6DpkWGES0Rx2eNOyYTf6ue75\nI6Ek6rlO+da5wMWr+3BvYMq4JMIwTHzAO+ZqqJPFpzKSiVuAWb2DOX/MDFecVWJE\ntF/R60lONxe4e/00MPCoDdqkLKdwROsk1yGL7z4Zk6jOWFEhIcWy/d2Ya5CpPvS3\nu4wNN4jkYAjra+8TiloRELhV4gpcEk8nkyNwLXOhYm7zQ5sIc5rfXoIrFzALB86W\nG05Nnqg+77zZIaTZpD9qekYlaEt+0OVtt9TTIeTiudQ983l6mfKwZYymrzymH1dL\nVgxBRYo+Z53QOSLiSKELfTBZxEoP1pBw6RiOHXydmJ/39hGgc2YAY/5ADwW2F2yb\nJ7+gxG6bPJ3ikDLYcD4CB5iJQdnTcDsFt3jyHAT6wOCzFAYPbHUqtzHfUM30dZBn\nnJhQF8udPLcXLaj6GW75JacCAwEAAQ==\n-----END PUBLIC KEY-----\n" }, diff --git a/test/fixtures/tesla_mock/https__info.pleroma.site_activity3.json b/test/fixtures/tesla_mock/https__info.pleroma.site_activity3.json index 1df73f2c5..dbf74dfe1 100644 --- a/test/fixtures/tesla_mock/https__info.pleroma.site_activity3.json +++ b/test/fixtures/tesla_mock/https__info.pleroma.site_activity3.json @@ -3,7 +3,7 @@ "attributedTo": "http://mastodon.example.org/users/admin", "attachment": [], "content": "

    this post was not actually written by Haelwenn

    ", - "id": "https://info.pleroma.site/activity2.json", + "id": "https://info.pleroma.site/activity3.json", "published": "2018-09-01T22:15:00Z", "tag": [], "to": [ diff --git a/test/fixtures/tesla_mock/kaniini@hubzilla.example.org.json b/test/fixtures/tesla_mock/kaniini@hubzilla.example.org.json index 11c79e11e..c354747cc 100644 --- a/test/fixtures/tesla_mock/kaniini@hubzilla.example.org.json +++ b/test/fixtures/tesla_mock/kaniini@hubzilla.example.org.json @@ -1 +1 @@ -{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://hubzilla.example.org/apschema/v1.2"],"type":"Person","id":"https://hubzilla.example.org/channel/kaniini","preferredUsername":"kaniini","name":"kaniini","icon":{"type":"Image","mediaType":"image/jpeg","url":"https://hubzilla.example.org/photo/profile/l/281","height":300,"width":300},"url":{"type":"Link","mediaType":"text/html","href":"https://hubzilla.example.org/channel/kaniini"},"inbox":"https://hubzilla.example.org/inbox/kaniini","outbox":"https://hubzilla.example.org/outbox/kaniini","followers":"https://hubzilla.example.org/followers/kaniini","following":"https://hubzilla.example.org/following/kaniini","endpoints":{"sharedInbox":"https://hubzilla.example.org/inbox"},"publicKey":{"id":"https://hubzilla.example.org/channel/kaniini/public_key_pem","owner":"https://hubzilla.example.org/channel/kaniini","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvXCDkQPw+1N8B2CUd5s2\nbYvjHt+t7soMNfUiRy0qGbgW46S45k5lCq1KpbFIX3sgGZ4OWjnXVbvjCJi4kl5M\nfm5DBXzpuu05AmjVl8hqk4GejajiE/1Nq0uWHPiOSFWispUjCzzCu65V+IsiE5JU\nvcL6WEf/pYNRq7gYqyT693F7+cO5/rVv9OScx5UOxbIuU1VXYhdHCqAMDJWadC89\nhePrcD3HOQKl06W2tDxHcWk6QjrdsUQGbNOgK/QIN9gSxA+rCFEvH5O0HAhI0aXq\ncOB+vysJUFLeQOAqmAKvKS5V6RqE1GqqT0pDWHack4EmQi0gkgVzo+45xoP6wfDl\nWwG88w21LNxGvGHuN4I8mg6cEoApqKQBSOj086UtfDfSlPC1B+PRD2phE5etucHd\nF/RIWN3SxVzU9BKIiaDm2gwOpvI8QuorQb6HDtZFO5NsSN3PnMnSywPe7kXl/469\nuQRYXrseqyOVIi6WjhvXkyWVKVE5CBz+S8wXHfKph+9YOyUcJeAVMijp9wrjBlMc\noSzOGu79oM7tpMSq/Xo6ePJ/glNOwZR+OKrg92Qp9BGTKDNwGrxuxP/9KwWtGLNf\nOMTtIkxtC3ubhxL3lBxOd7l+Bmum0UJV2f8ogkCgvTpIz05jMoyU8qWl6kkWNQlY\nDropXWaOfy7Lac+G4qlfSgsCAwEAAQ==\n-----END PUBLIC KEY-----\n"},"nomadicLocations":[{"id":"https://hubzilla.example.org/locs/kaniini","type":"nomadicLocation","locationAddress":"acct:kaniini@hubzilla.example.org","locationPrimary":true,"locationDeleted":false}],"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"6b981a2f3bdcffc20252e3b131d4a4569fd2dea9fac543e5196136302f492694","creator":"https://hubzilla.example.org/channel/kaniini/public_key_pem","created":"2018-05-19T08:19:13Z","signatureValue":"ezpT4iCIUzJSeJa/Jsf4EkgbX9enWZG/0eliLXZcvkeCX9mZabaX9LMQRViP2GSlAJBHJu+UqK5LWaoWw9pYkQQHUL+43w2DeBxQicEcPqpT46j6pHuWptfwB8YHTC2/Pb56Y/jseU37j+FW8xVmcGZk4cPqJRLQNojwJlQiFOpBEd4Cel6081W12Pep578+6xBL+h92RJsWznA1gE/NV9dkCqoAoNdiORJg68sVTm0yYxPit2D/DLwXUFeBhC47EZtY3DtAOf7rADGwbquXKug/wtEI47R4p9dJvMWERSVW9O2FmDk8deUjRR3qO1iYGce8O+uMnnBHmuTcToRUHH7mxfMdqjfbcZ9DGBjKtLPSOyVPT9rENeyX8fsksmX0XhfHsNSWkmeDaU5/Au3IY75gDewiGzmzLOpRc6GUnHHro7lMpyMuo3lLZKjNVsFZbx+sXCYwORz5GAMuwIt/iCUdrsQsF5aycqfUAZrFBPguH6DVjbMUqyLvS78sDKiWqgWVhq9VDKse+WuQaJLGBDJNF9APoA6NDMjjIBZfmkGf2mV7ubIYihoOncUjahFqxU5306cNxAcdj2uNcwkgX4BCnBe/L2YsvMHhZrupzDewWWy4fxhktyoZ7VhLSl1I7fMPytjOpb9EIvng4DHGX2t+hKfon2rCGfECPavwiTM="}} +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://hubzilla.example.org/apschema/v1.2"],"type":"Person","id":"https://hubzilla.example.org/channel/kaniini","preferredUsername":"kaniini","name":"kaniini","icon":{"type":"Image","mediaType":"image/jpeg","url":"https://hubzilla.example.org/photo/profile/l/281","height":300,"width":300},"url":{"type":"Link","mediaType":"text/html","href":"https://hubzilla.example.org/channel/kaniini"},"inbox":"https://hubzilla.example.org/inbox/kaniini","outbox":"https://hubzilla.example.org/outbox/kaniini","followers":"https://hubzilla.example.org/followers/kaniini","following":"https://hubzilla.example.org/following/kaniini","endpoints":{"sharedInbox":"https://hubzilla.example.org/inbox"},"publicKey":{"id":"https://hubzilla.example.org/channel/kaniini","owner":"https://hubzilla.example.org/channel/kaniini","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvXCDkQPw+1N8B2CUd5s2\nbYvjHt+t7soMNfUiRy0qGbgW46S45k5lCq1KpbFIX3sgGZ4OWjnXVbvjCJi4kl5M\nfm5DBXzpuu05AmjVl8hqk4GejajiE/1Nq0uWHPiOSFWispUjCzzCu65V+IsiE5JU\nvcL6WEf/pYNRq7gYqyT693F7+cO5/rVv9OScx5UOxbIuU1VXYhdHCqAMDJWadC89\nhePrcD3HOQKl06W2tDxHcWk6QjrdsUQGbNOgK/QIN9gSxA+rCFEvH5O0HAhI0aXq\ncOB+vysJUFLeQOAqmAKvKS5V6RqE1GqqT0pDWHack4EmQi0gkgVzo+45xoP6wfDl\nWwG88w21LNxGvGHuN4I8mg6cEoApqKQBSOj086UtfDfSlPC1B+PRD2phE5etucHd\nF/RIWN3SxVzU9BKIiaDm2gwOpvI8QuorQb6HDtZFO5NsSN3PnMnSywPe7kXl/469\nuQRYXrseqyOVIi6WjhvXkyWVKVE5CBz+S8wXHfKph+9YOyUcJeAVMijp9wrjBlMc\noSzOGu79oM7tpMSq/Xo6ePJ/glNOwZR+OKrg92Qp9BGTKDNwGrxuxP/9KwWtGLNf\nOMTtIkxtC3ubhxL3lBxOd7l+Bmum0UJV2f8ogkCgvTpIz05jMoyU8qWl6kkWNQlY\nDropXWaOfy7Lac+G4qlfSgsCAwEAAQ==\n-----END PUBLIC KEY-----\n"},"nomadicLocations":[{"id":"https://hubzilla.example.org/locs/kaniini","type":"nomadicLocation","locationAddress":"acct:kaniini@hubzilla.example.org","locationPrimary":true,"locationDeleted":false}],"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"6b981a2f3bdcffc20252e3b131d4a4569fd2dea9fac543e5196136302f492694","creator":"https://hubzilla.example.org/channel","created":"2018-05-19T08:19:13Z","signatureValue":"ezpT4iCIUzJSeJa/Jsf4EkgbX9enWZG/0eliLXZcvkeCX9mZabaX9LMQRViP2GSlAJBHJu+UqK5LWaoWw9pYkQQHUL+43w2DeBxQicEcPqpT46j6pHuWptfwB8YHTC2/Pb56Y/jseU37j+FW8xVmcGZk4cPqJRLQNojwJlQiFOpBEd4Cel6081W12Pep578+6xBL+h92RJsWznA1gE/NV9dkCqoAoNdiORJg68sVTm0yYxPit2D/DLwXUFeBhC47EZtY3DtAOf7rADGwbquXKug/wtEI47R4p9dJvMWERSVW9O2FmDk8deUjRR3qO1iYGce8O+uMnnBHmuTcToRUHH7mxfMdqjfbcZ9DGBjKtLPSOyVPT9rENeyX8fsksmX0XhfHsNSWkmeDaU5/Au3IY75gDewiGzmzLOpRc6GUnHHro7lMpyMuo3lLZKjNVsFZbx+sXCYwORz5GAMuwIt/iCUdrsQsF5aycqfUAZrFBPguH6DVjbMUqyLvS78sDKiWqgWVhq9VDKse+WuQaJLGBDJNF9APoA6NDMjjIBZfmkGf2mV7ubIYihoOncUjahFqxU5306cNxAcdj2uNcwkgX4BCnBe/L2YsvMHhZrupzDewWWy4fxhktyoZ7VhLSl1I7fMPytjOpb9EIvng4DHGX2t+hKfon2rCGfECPavwiTM="}} diff --git a/test/fixtures/tesla_mock/relay@mastdon.example.org.json b/test/fixtures/tesla_mock/relay@mastdon.example.org.json index c1fab7d3b..21dd405c8 100644 --- a/test/fixtures/tesla_mock/relay@mastdon.example.org.json +++ b/test/fixtures/tesla_mock/relay@mastdon.example.org.json @@ -11,7 +11,7 @@ "toot": "http://joinmastodon.org/ns#", "Emoji": "toot:Emoji" }], - "id": "http://mastodon.example.org/users/admin", + "id": "http://mastodon.example.org/users/relay", "type": "Application", "invisible": true, "following": "http://mastodon.example.org/users/admin/following", @@ -24,8 +24,8 @@ "url": "http://mastodon.example.org/@admin", "manuallyApprovesFollowers": false, "publicKey": { - "id": "http://mastodon.example.org/users/admin#main-key", - "owner": "http://mastodon.example.org/users/admin", + "id": "http://mastodon.example.org/users/relay#main-key", + "owner": "http://mastodon.example.org/users/relay", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n" }, "attachment": [{ diff --git a/test/fixtures/users_mock/friendica_followers.json b/test/fixtures/users_mock/friendica_followers.json index 7b86b5fe2..02b287e23 100644 --- a/test/fixtures/users_mock/friendica_followers.json +++ b/test/fixtures/users_mock/friendica_followers.json @@ -13,7 +13,7 @@ "directMessage": "litepub:directMessage" } ], - "id": "http://localhost:8080/followers/fuser3", + "id": "http://remote.org/followers/fuser3", "type": "OrderedCollection", "totalItems": 296 } diff --git a/test/fixtures/users_mock/friendica_following.json b/test/fixtures/users_mock/friendica_following.json index 7c526befc..0908e78f0 100644 --- a/test/fixtures/users_mock/friendica_following.json +++ b/test/fixtures/users_mock/friendica_following.json @@ -13,7 +13,7 @@ "directMessage": "litepub:directMessage" } ], - "id": "http://localhost:8080/following/fuser3", + "id": "http://remote.org/following/fuser3", "type": "OrderedCollection", "totalItems": 32 } diff --git a/test/fixtures/users_mock/masto_closed_followers.json b/test/fixtures/users_mock/masto_closed_followers.json index da296892d..ccc32d15e 100644 --- a/test/fixtures/users_mock/masto_closed_followers.json +++ b/test/fixtures/users_mock/masto_closed_followers.json @@ -1,7 +1,7 @@ { "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4001/users/masto_closed/followers", + "id": "http://remote.org/users/masto_closed/followers", "type": "OrderedCollection", "totalItems": 437, - "first": "http://localhost:4001/users/masto_closed/followers?page=1" + "first": "http://remote.org/users/masto_closed/followers?page=1" } diff --git a/test/fixtures/users_mock/masto_closed_followers_page.json b/test/fixtures/users_mock/masto_closed_followers_page.json index 04ab0c4d3..e4f1b3ac0 100644 --- a/test/fixtures/users_mock/masto_closed_followers_page.json +++ b/test/fixtures/users_mock/masto_closed_followers_page.json @@ -1 +1 @@ -{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"http://localhost:4001/users/masto_closed/followers?page=2","partOf":"http://localhost:4001/users/masto_closed/followers","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]} +{"@context":"https://www.w3.org/ns/activitystreams","id":"http://remote.org/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"http://remote.org/users/masto_closed/followers?page=2","partOf":"http://remote.org/users/masto_closed/followers","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]} diff --git a/test/fixtures/users_mock/masto_closed_following.json b/test/fixtures/users_mock/masto_closed_following.json index 146d49f9c..34e9e9fe8 100644 --- a/test/fixtures/users_mock/masto_closed_following.json +++ b/test/fixtures/users_mock/masto_closed_following.json @@ -1,7 +1,7 @@ { "@context": "https://www.w3.org/ns/activitystreams", - "id": "http://localhost:4001/users/masto_closed/following", + "id": "http://remote.org/users/masto_closed/following", "type": "OrderedCollection", "totalItems": 152, - "first": "http://localhost:4001/users/masto_closed/following?page=1" + "first": "http://remote.org/users/masto_closed/following?page=1" } diff --git a/test/fixtures/users_mock/masto_closed_following_page.json b/test/fixtures/users_mock/masto_closed_following_page.json index 8d8324699..d398ae3cf 100644 --- a/test/fixtures/users_mock/masto_closed_following_page.json +++ b/test/fixtures/users_mock/masto_closed_following_page.json @@ -1 +1 @@ -{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"http://localhost:4001/users/masto_closed/following?page=2","partOf":"http://localhost:4001/users/masto_closed/following","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]} +{"@context":"https://www.w3.org/ns/activitystreams","id":"http://remote.org/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"http://remote.org/users/masto_closed/following?page=2","partOf":"http://remote.org/users/masto_closed/following","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]} diff --git a/test/fixtures/users_mock/pleroma_followers.json b/test/fixtures/users_mock/pleroma_followers.json index db71d084b..9611990ee 100644 --- a/test/fixtures/users_mock/pleroma_followers.json +++ b/test/fixtures/users_mock/pleroma_followers.json @@ -1,14 +1,14 @@ { "type": "OrderedCollection", "totalItems": 527, - "id": "http://localhost:4001/users/fuser2/followers", + "id": "http://remote.org/users/fuser2/followers", "first": { "type": "OrderedCollectionPage", "totalItems": 527, - "partOf": "http://localhost:4001/users/fuser2/followers", + "partOf": "http://remote.org/users/fuser2/followers", "orderedItems": [], - "next": "http://localhost:4001/users/fuser2/followers?page=2", - "id": "http://localhost:4001/users/fuser2/followers?page=1" + "next": "http://remote.org/users/fuser2/followers?page=2", + "id": "http://remote.org/users/fuser2/followers?page=1" }, "@context": [ "https://www.w3.org/ns/activitystreams", diff --git a/test/fixtures/users_mock/pleroma_following.json b/test/fixtures/users_mock/pleroma_following.json index 33d087703..27fadbc94 100644 --- a/test/fixtures/users_mock/pleroma_following.json +++ b/test/fixtures/users_mock/pleroma_following.json @@ -1,14 +1,14 @@ { "type": "OrderedCollection", "totalItems": 267, - "id": "http://localhost:4001/users/fuser2/following", + "id": "http://remote.org/users/fuser2/following", "first": { "type": "OrderedCollectionPage", "totalItems": 267, - "partOf": "http://localhost:4001/users/fuser2/following", + "partOf": "http://remote.org/users/fuser2/following", "orderedItems": [], - "next": "http://localhost:4001/users/fuser2/following?page=2", - "id": "http://localhost:4001/users/fuser2/following?page=1" + "next": "http://remote.org/users/fuser2/following?page=2", + "id": "http://remote.org/users/fuser2/following?page=1" }, "@context": [ "https://www.w3.org/ns/activitystreams", diff --git a/test/fixtures/xml_normal.xml b/test/fixtures/xml_normal.xml new file mode 100644 index 000000000..8dfa0af54 --- /dev/null +++ b/test/fixtures/xml_normal.xml @@ -0,0 +1,44 @@ + + + + + Match One + + + 1 + Team One + + + 2 + Team Two + + + + + Match Two + + + 2 + Team Two + + + 3 + Team Three + + + + + Match Three + + + 1 + Team One + + + 3 + Team Three + + + + + diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index 46c109cc1..3b09f656b 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -182,8 +182,81 @@ test "load a settings with large values and pass to file", %{temp_file: temp_fil assert File.exists?(temp_file) {:ok, file} = File.read(temp_file) - assert file == - "import Config\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" + {:ok, file_quote} = Code.string_to_quoted(file) + + {:__block__, [], + [ + _, + {:config, _, config_quote} + ]} = file_quote + + assert [ + :pleroma, + :instance, + [ + name: "Pleroma", + email: "example@example.com", + notify_email: "noreply@example.com", + description: "A Pleroma instance, an alternative fediverse server", + limit: 5000, + remote_limit: 100_000, + upload_limit: 16_000_000, + avatar_upload_limit: 2_000_000, + background_upload_limit: 4_000_000, + banner_upload_limit: 4_000_000, + poll_limits: {_, _, poll_limits}, + registrations_open: true, + federating: true, + federation_incoming_replies_max_depth: 100, + federation_reachability_timeout_days: 7, + federation_publisher_modules: [ + {_, _, [:Pleroma, :Web, :ActivityPub, :Publisher]} + ], + allow_relay: true, + public: true, + quarantined_instances: [], + managed_config: true, + static_dir: "instance/static/", + allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], + autofollowed_nicknames: [], + max_pinned_statuses: 1, + attachment_links: false, + max_report_comment_size: 1000, + safe_dm_mentions: false, + healthcheck: false, + remote_post_retention_days: 90, + skip_thread_containment: true, + limit_to_local_content: :unauthenticated, + user_bio_length: 5000, + user_name_length: 100, + max_account_fields: 10, + max_remote_account_fields: 20, + account_field_name_length: 512, + account_field_value_length: 2048, + external_user_synchronization: true, + extended_nickname_format: true, + multi_factor_authentication: [ + {:totp, + [ + digits: 6, + period: 30 + ]}, + {:backup_codes, + [ + number: 2, + length: 6 + ]} + ] + ] + ] = config_quote + + assert Keyword.equal?( + poll_limits, + max_options: 20, + max_option_chars: 200, + min_expiration: 0, + max_expiration: 31_536_000 + ) end end diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 40c5fd402..97fa830ff 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -371,8 +371,6 @@ test "We don't have unexpected tables which may contain objects that are referen ["apps"], ["backups"], ["bookmarks"], - ["chat_message_references"], - ["chats"], ["config"], ["conversation_participation_recipient_ships"], ["conversation_participations"], diff --git a/test/mix/tasks/pleroma/instance_test.exs b/test/mix/tasks/pleroma/instance_test.exs index 5a5a68053..17b2e3267 100644 --- a/test/mix/tasks/pleroma/instance_test.exs +++ b/test/mix/tasks/pleroma/instance_test.exs @@ -39,6 +39,8 @@ test "running gen" do tmp_path() <> "setup.psql", "--domain", "test.pleroma.social", + "--media-url", + "https://media.pleroma.social/media", "--instance-name", "Pleroma", "--admin-email", @@ -69,8 +71,6 @@ test "running gen" do "./test/../test/instance/static/", "--strip-uploads", "y", - "--dedupe-uploads", - "n", "--anonymize-uploads", "n" ]) @@ -92,6 +92,7 @@ test "running gen" do assert generated_config =~ "configurable_from_database: true" assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]" assert generated_config =~ "filters: [Pleroma.Upload.Filter.Exiftool]" + assert generated_config =~ "base_url: \"https://media.pleroma.social/media\"" assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql() assert File.exists?(Path.expand("./test/instance/static/robots.txt")) end diff --git a/test/mix/tasks/pleroma/uploads_test.exs b/test/mix/tasks/pleroma/uploads_test.exs index 67fb642c1..d00e25a37 100644 --- a/test/mix/tasks/pleroma/uploads_test.exs +++ b/test/mix/tasks/pleroma/uploads_test.exs @@ -16,7 +16,6 @@ defmodule Mix.Tasks.Pleroma.UploadsTest do Mix.shell(Mix.Shell.IO) end) - File.mkdir_p!("test/uploads") :ok end diff --git a/test/pleroma/activity/pruner_test.exs b/test/pleroma/activity/pruner_test.exs index e8d4b30aa..26db60c1c 100644 --- a/test/pleroma/activity/pruner_test.exs +++ b/test/pleroma/activity/pruner_test.exs @@ -6,22 +6,52 @@ defmodule Pleroma.Activity.PrunerTest do import Pleroma.Factory - describe "prune_deletes" do - test "it prunes old delete objects" do + describe "prune_transient_activities" do + test "it prunes old transient activities" do user = insert(:user) + old_time = DateTime.utc_now() |> DateTime.add(-31 * 24, :hour) new_delete = insert(:delete_activity, type: "Delete", user: user) old_delete = insert(:delete_activity, - type: "Delete", user: user, - inserted_at: DateTime.utc_now() |> DateTime.add(-31 * 24, :hour) + inserted_at: old_time ) + new_update = insert(:update_activity, type: "Update", user: user) + + old_update = + insert(:update_activity, + type: "Update", + user: user, + inserted_at: old_time + ) + + new_undo = insert(:undo_activity) + + old_undo = insert(:undo_activity, inserted_at: old_time) + + new_remove = insert(:remove_activity) + + old_remove = insert(:remove_activity, inserted_at: old_time) + Pruner.prune_deletes() + Pruner.prune_updates() + Pruner.prune_undos() + Pruner.prune_removes() + assert Activity.get_by_id(new_delete.id) refute Activity.get_by_id(old_delete.id) + + assert Activity.get_by_id(new_update.id) + refute Activity.get_by_id(old_update.id) + + assert Activity.get_by_id(new_undo.id) + refute Activity.get_by_id(old_undo.id) + + assert Activity.get_by_id(new_remove.id) + refute Activity.get_by_id(old_remove.id) end end diff --git a/test/pleroma/collections/collections_fetcher_test.exs b/test/pleroma/collections/collections_fetcher_test.exs index 7a582a3d7..ff1aa84db 100644 --- a/test/pleroma/collections/collections_fetcher_test.exs +++ b/test/pleroma/collections/collections_fetcher_test.exs @@ -12,11 +12,14 @@ defmodule Akkoma.Collections.FetcherTest do end test "it should extract items from an embedded array in a Collection" do + ap_id = "https://example.com/collection/ordered_array" + unordered_collection = "test/fixtures/collections/unordered_array.json" |> File.read!() - - ap_id = "https://example.com/collection/ordered_array" + |> Jason.decode!() + |> Map.put("id", ap_id) + |> Jason.encode!(pretty: true) Tesla.Mock.mock(fn %{ diff --git a/test/pleroma/emails/admin_email_test.exs b/test/pleroma/emails/admin_email_test.exs index e65752c23..6b0acf817 100644 --- a/test/pleroma/emails/admin_email_test.exs +++ b/test/pleroma/emails/admin_email_test.exs @@ -7,7 +7,6 @@ defmodule Pleroma.Emails.AdminEmailTest do import Pleroma.Factory alias Pleroma.Emails.AdminEmail - alias Pleroma.Web.Router.Helpers test "build report email" do config = Pleroma.Config.get(:instance) @@ -18,7 +17,7 @@ test "build report email" do res = AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment") - status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, "12") + status_url = url(~p[/notice/12]) reporter_url = reporter.ap_id account_url = account.ap_id diff --git a/test/pleroma/emails/user_email_test.exs b/test/pleroma/emails/user_email_test.exs index 564552004..ee76dc45c 100644 --- a/test/pleroma/emails/user_email_test.exs +++ b/test/pleroma/emails/user_email_test.exs @@ -6,8 +6,6 @@ defmodule Pleroma.Emails.UserEmailTest do use Pleroma.DataCase, async: true alias Pleroma.Emails.UserEmail - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Router import Pleroma.Factory @@ -18,7 +16,7 @@ test "build password reset email" do assert email.from == {config[:name], config[:notify_email]} assert email.to == [{user.name, user.email}] assert email.subject == "Password reset" - assert email.html_body =~ Router.Helpers.reset_password_url(Endpoint, :reset, "test_token") + assert email.html_body =~ url(~p"/api/v1/pleroma/password_reset/test_token") end test "build user invitation email" do @@ -30,8 +28,7 @@ test "build user invitation email" do assert email.subject == "Invitation to Akkoma" assert email.to == [{"Jonh", "test@test.com"}] - assert email.html_body =~ - Router.Helpers.redirect_url(Endpoint, :registration_page, token.token) + assert email.html_body =~ url(~p[/registration/#{token.token}]) end test "build account confirmation email" do @@ -42,8 +39,7 @@ test "build account confirmation email" do assert email.to == [{user.name, user.email}] assert email.subject == "#{config[:name]} account confirmation" - assert email.html_body =~ - Router.Helpers.confirm_email_url(Endpoint, :confirm_email, user.id, "conf-token") + assert email.html_body =~ url(~p[/api/account/confirm_email/#{user.id}/conf-token]) end test "build approval pending email" do diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index 4d769789d..f5d2e2eef 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -93,7 +93,9 @@ test "add emoji file", %{pack: pack} do assert updated_pack.files_count == 1 end - test "load_pack/1 ignores path traversal in a forged pack name", %{pack: pack} do - assert {:ok, ^pack} = Pack.load_pack("../../../../../dump_pack") + test "load_pack/1 panics on path traversal in a forged pack name" do + assert_raise(RuntimeError, "Invalid or malicious pack name: ../../../../../dump_pack", fn -> + Pack.load_pack("../../../../../dump_pack") + end) end end diff --git a/test/pleroma/healthcheck_test.exs b/test/pleroma/healthcheck_test.exs index 469e5b397..9352840b5 100644 --- a/test/pleroma/healthcheck_test.exs +++ b/test/pleroma/healthcheck_test.exs @@ -6,17 +6,19 @@ defmodule Pleroma.HealthcheckTest do use Pleroma.DataCase, async: true alias Pleroma.Healthcheck + import Pleroma.Test.Matchers.List + test "system_info/0" do result = Healthcheck.system_info() |> Map.from_struct() - assert Map.keys(result) == [ - :active, - :healthy, - :idle, - :job_queue_stats, - :memory_used, - :pool_size - ] + assert_unordered_list_equal(Map.keys(result), [ + :active, + :healthy, + :idle, + :job_queue_stats, + :memory_used, + :pool_size + ]) end describe "check_health/1" do diff --git a/test/pleroma/integration/federation_test.exs b/test/pleroma/integration/federation_test.exs index da433e2c0..e4e37aa9f 100644 --- a/test/pleroma/integration/federation_test.exs +++ b/test/pleroma/integration/federation_test.exs @@ -31,10 +31,9 @@ test "within/2 captures local bindings and executes block on remote node" do test "runs webserver on customized port" do {nickname, url, url_404} = within @federated1 do - import Pleroma.Web.Router.Helpers user = Pleroma.Factory.insert(:user) - user_url = account_url(Pleroma.Web.Endpoint, :show, user) - url_404 = account_url(Pleroma.Web.Endpoint, :show, "not-exists") + user_url = ~p[/api/v1/accounts/#{user}] + url_404 = ~p"/api/v1/accounts/not-exists" {user.nickname, user_url, url_404} end diff --git a/test/pleroma/mfa/totp_test.exs b/test/pleroma/mfa/totp_test.exs index 828993866..b0572ad80 100644 --- a/test/pleroma/mfa/totp_test.exs +++ b/test/pleroma/mfa/totp_test.exs @@ -6,16 +6,19 @@ defmodule Pleroma.MFA.TOTPTest do use Pleroma.DataCase, async: true alias Pleroma.MFA.TOTP + import Pleroma.Test.Matchers.URI test "create provisioning_uri to generate qrcode" do uri = - TOTP.provisioning_uri("test-secrcet", "test@example.com", + TOTP.provisioning_uri("test-secret", "test@example.com", issuer: "Plerome-42", digits: 8, period: 60 ) - assert uri == - "otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secrcet" + assert_uri_equals( + uri, + "otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secret" + ) end end diff --git a/test/pleroma/object/containment_test.exs b/test/pleroma/object/containment_test.exs index fb2fb7d49..f8f40a3ac 100644 --- a/test/pleroma/object/containment_test.exs +++ b/test/pleroma/object/containment_test.exs @@ -17,16 +17,58 @@ defmodule Pleroma.Object.ContainmentTest do end describe "general origin containment" do - test "works for completely actorless posts" do - assert :error == - Containment.contain_origin("https://glaceon.social/users/monorail", %{ + test "handles completly actorless objects gracefully" do + assert :ok == + Containment.contain_origin("https://glaceon.social/statuses/123", %{ "deleted" => "2019-10-30T05:48:50.249606Z", "formerType" => "Note", - "id" => "https://glaceon.social/users/monorail/statuses/103049757364029187", + "id" => "https://glaceon.social/statuses/123", "type" => "Tombstone" }) end + test "errors for spoofed actors" do + assert :error == + Containment.contain_origin("https://glaceon.social/statuses/123", %{ + "actor" => "https://otp.akkoma.dev/users/you", + "id" => "https://glaceon.social/statuses/123", + "type" => "Note" + }) + end + + test "errors for spoofed attributedTo" do + assert :error == + Containment.contain_origin("https://glaceon.social/statuses/123", %{ + "attributedTo" => "https://otp.akkoma.dev/users/you", + "id" => "https://glaceon.social/statuses/123", + "type" => "Note" + }) + end + + test "accepts valid actors" do + assert :ok == + Containment.contain_origin("https://glaceon.social/statuses/123", %{ + "actor" => "https://glaceon.social/users/monorail", + "attributedTo" => "https://glaceon.social/users/monorail", + "id" => "https://glaceon.social/statuses/123", + "type" => "Note" + }) + + assert :ok == + Containment.contain_origin("https://glaceon.social/statuses/123", %{ + "actor" => "https://glaceon.social/users/monorail", + "id" => "https://glaceon.social/statuses/123", + "type" => "Note" + }) + + assert :ok == + Containment.contain_origin("https://glaceon.social/statuses/123", %{ + "attributedTo" => "https://glaceon.social/users/monorail", + "id" => "https://glaceon.social/statuses/123", + "type" => "Note" + }) + end + test "contain_origin_from_id() catches obvious spoofing attempts" do data = %{ "id" => "http://example.com/~alyssa/activities/1234.json" @@ -63,6 +105,56 @@ test "contain_origin_from_id() allows matching IDs" do ) end + test "contain_id_to_fetch() refuses alternate IDs within the same origin domain" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json", + "url" => "http://example.com/@alyssa/status/1234" + } + + :error = + Containment.contain_id_to_fetch( + "http://example.com/~alyssa/activities/1234", + data + ) + end + + test "contain_id_to_fetch() allows matching IDs" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json/" + } + + :ok = + Containment.contain_id_to_fetch( + "http://example.com/~alyssa/activities/1234.json/", + data + ) + + :ok = + Containment.contain_id_to_fetch( + "http://example.com/~alyssa/activities/1234.json", + data + ) + end + + test "contain_id_to_fetch() allows display URLs" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json", + "url" => "http://example.com/@alyssa/status/1234" + } + + :ok = + Containment.contain_id_to_fetch( + "http://example.com/@alyssa/status/1234", + data + ) + + :ok = + Containment.contain_id_to_fetch( + "http://example.com/@alyssa/status/1234/", + data + ) + end + test "users cannot be collided through fake direction spoofing attempts" do _user = insert(:user, %{ diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs index 8cf0bce48..4c4831af3 100644 --- a/test/pleroma/object/fetcher_test.exs +++ b/test/pleroma/object/fetcher_test.exs @@ -14,6 +14,17 @@ defmodule Pleroma.Object.FetcherTest do import Mock import Tesla.Mock + defp spoofed_object_with_ids( + id \\ "https://patch.cx/objects/spoof", + actor_id \\ "https://patch.cx/users/rin" + ) do + File.read!("test/fixtures/spoofed-object.json") + |> Jason.decode!() + |> Map.put("id", id) + |> Map.put("actor", actor_id) + |> Jason.encode!() + end + setup do mock(fn %{method: :get, url: "https://mastodon.example.org/users/userisgone"} -> @@ -22,6 +33,32 @@ defmodule Pleroma.Object.FetcherTest do %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} -> %Tesla.Env{status: 404} + # Spoof: wrong Content-Type + %{ + method: :get, + url: "https://patch.cx/objects/spoof_content_type.json" + } -> + %Tesla.Env{ + status: 200, + url: "https://patch.cx/objects/spoof_content_type.json", + headers: [{"content-type", "application/json"}], + body: spoofed_object_with_ids("https://patch.cx/objects/spoof_content_type.json") + } + + # Spoof: no Content-Type + %{ + method: :get, + url: "https://patch.cx/objects/spoof_content_type" + } -> + %Tesla.Env{ + status: 200, + url: "https://patch.cx/objects/spoof_content_type", + headers: [], + body: spoofed_object_with_ids("https://patch.cx/objects/spoof_content_type") + } + + # Spoof: mismatching ids + # Variant 1: Non-exisitng fake id %{ method: :get, url: @@ -29,8 +66,75 @@ defmodule Pleroma.Object.FetcherTest do } -> %Tesla.Env{ status: 200, - headers: [{"content-type", "application/json"}], - body: File.read!("test/fixtures/spoofed-object.json") + url: + "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json", + headers: [{"content-type", "application/activity+json"}], + body: spoofed_object_with_ids() + } + + %{method: :get, url: "https://patch.cx/objects/spoof"} -> + %Tesla.Env{ + status: 404, + url: "https://patch.cx/objects/spoof", + headers: [], + body: "Not found" + } + + # Varaint 2: two-stage payload + %{method: :get, url: "https://patch.cx/media/spoof_stage1.json"} -> + %Tesla.Env{ + status: 200, + url: "https://patch.cx/media/spoof_stage1.json", + headers: [{"content-type", "application/activity+json"}], + body: spoofed_object_with_ids("https://patch.cx/media/spoof_stage2.json") + } + + %{method: :get, url: "https://patch.cx/media/spoof_stage2.json"} -> + %Tesla.Env{ + status: 200, + url: "https://patch.cx/media/spoof_stage2.json", + headers: [{"content-type", "application/activity+json"}], + body: spoofed_object_with_ids("https://patch.cx/media/unpredictable.json") + } + + # Spoof: cross-domain redirect with original domain id + %{method: :get, url: "https://patch.cx/objects/spoof_media_redirect1"} -> + %Tesla.Env{ + status: 200, + url: "https://media.patch.cx/objects/spoof", + headers: [{"content-type", "application/activity+json"}], + body: spoofed_object_with_ids("https://patch.cx/objects/spoof_media_redirect1") + } + + # Spoof: cross-domain redirect with final domain id + %{method: :get, url: "https://patch.cx/objects/spoof_media_redirect2"} -> + %Tesla.Env{ + status: 200, + url: "https://media.patch.cx/objects/spoof_media_redirect2", + headers: [{"content-type", "application/activity+json"}], + body: spoofed_object_with_ids("https://media.patch.cx/objects/spoof_media_redirect2") + } + + # No-Spoof: same domain redirect + %{method: :get, url: "https://patch.cx/objects/spoof_redirect"} -> + %Tesla.Env{ + status: 200, + url: "https://patch.cx/objects/spoof_redirect", + headers: [{"content-type", "application/activity+json"}], + body: spoofed_object_with_ids("https://patch.cx/objects/spoof_redirect") + } + + # Spoof: Actor from another domain + %{method: :get, url: "https://patch.cx/objects/spoof_foreign_actor"} -> + %Tesla.Env{ + status: 200, + url: "https://patch.cx/objects/spoof_foreign_actor", + headers: [{"content-type", "application/activity+json"}], + body: + spoofed_object_with_ids( + "https://patch.cx/objects/spoof_foreign_actor", + "https://not.patch.cx/users/rin" + ) } env -> @@ -46,6 +150,7 @@ defmodule Pleroma.Object.FetcherTest do %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} -> %Tesla.Env{ status: 200, + url: "https://social.sakamoto.gq/objects/f20f2497-66d9-4a52-a2e1-1be2a39c32c1", body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"), headers: HttpRequestMock.activitypub_object_headers() } @@ -129,6 +234,71 @@ test "it rejects objects when attributedTo is wrong (variant 2)" do end end + describe "fetcher security and auth checks" do + test "it does not fetch a spoofed object without content type" do + assert {:error, {:content_type, nil}} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://patch.cx/objects/spoof_content_type" + ) + end + + test "it does not fetch a spoofed object with wrong content type" do + assert {:error, {:content_type, _}} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://patch.cx/objects/spoof_content_type.json" + ) + end + + test "it does not fetch a spoofed object with id different from URL" do + assert {:error, "Object's ActivityPub id/url does not match final fetch URL"} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json" + ) + + assert {:error, "Object's ActivityPub id/url does not match final fetch URL"} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://patch.cx/media/spoof_stage1.json" + ) + end + + test "it does not fetch an object via cross-domain redirects (initial id)" do + assert {:error, {:cross_domain_redirect, true}} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://patch.cx/objects/spoof_media_redirect1" + ) + end + + test "it does not fetch an object via cross-domain redirects (final id)" do + assert {:error, {:cross_domain_redirect, true}} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://patch.cx/objects/spoof_media_redirect2" + ) + end + + test "it accepts same-domain redirects" do + assert {:ok, %{"id" => id} = _object} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://patch.cx/objects/spoof_redirect" + ) + + assert id == "https://patch.cx/objects/spoof_redirect" + end + + test "it does not fetch a spoofed object with a foreign actor" do + assert {:error, "Object containment failed."} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://patch.cx/objects/spoof_foreign_actor" + ) + end + + test "it does not fetch from localhost" do + assert {:error, "Trying to fetch local resource"} = + Fetcher.fetch_and_contain_remote_object_from_id( + Pleroma.Web.Endpoint.url() <> "/spoof_local" + ) + end + end + describe "fetching an object" do test "it fetches an object" do {:ok, object} = @@ -155,13 +325,6 @@ test "Return MRF reason when fetched status is rejected by one" do ) end - test "it does not fetch a spoofed object uploaded on an instance as an attachment" do - assert {:error, _} = - Fetcher.fetch_object_from_id( - "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json" - ) - end - test "does not fetch anything from a rejected instance" do clear_config([:mrf_simple, :reject], [{"evil.example.org", "i said so"}]) @@ -583,12 +746,13 @@ test "should return ok if the content type is application/activity+json" do } -> %Tesla.Env{ status: 200, + url: "https://mastodon.social/2", headers: [{"content-type", "application/activity+json"}], body: "{}" } end) - assert {:ok, "{}"} = Fetcher.get_object("https://mastodon.social/2") + assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2") end test "should return ok if the content type is application/ld+json with a profile" do @@ -599,6 +763,7 @@ test "should return ok if the content type is application/ld+json with a profile } -> %Tesla.Env{ status: 200, + url: "https://mastodon.social/2", headers: [ {"content-type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""} @@ -607,24 +772,7 @@ test "should return ok if the content type is application/ld+json with a profile } end) - assert {:ok, "{}"} = Fetcher.get_object("https://mastodon.social/2") - - Tesla.Mock.mock(fn - %{ - method: :get, - url: "https://mastodon.social/2" - } -> - %Tesla.Env{ - status: 200, - headers: [ - {"content-type", - "application/ld+json; profile=\"http://www.w3.org/ns/activitystreams\""} - ], - body: "{}" - } - end) - - assert {:ok, "{}"} = Fetcher.get_object("https://mastodon.social/2") + assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2") end test "should not return ok with other content types" do @@ -635,6 +783,7 @@ test "should not return ok with other content types" do } -> %Tesla.Env{ status: 200, + url: "https://mastodon.social/2", headers: [{"content-type", "application/json"}], body: "{}" } @@ -643,5 +792,23 @@ test "should not return ok with other content types" do assert {:error, {:content_type, "application/json"}} = Fetcher.get_object("https://mastodon.social/2") end + + test "returns the url after redirects" do + Tesla.Mock.mock(fn + %{ + method: :get, + url: "https://mastodon.social/5" + } -> + %Tesla.Env{ + status: 200, + url: "https://mastodon.social/7", + headers: [{"content-type", "application/activity+json"}], + body: "{}" + } + end) + + assert {:ok, "https://mastodon.social/7", "{}"} = + Fetcher.get_object("https://mastodon.social/5") + end end end diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs index 8320660a5..4b0fec1bd 100644 --- a/test/pleroma/object_test.exs +++ b/test/pleroma/object_test.exs @@ -22,6 +22,13 @@ defmodule Pleroma.ObjectTest do :ok end + # Only works for a single attachment but that's all we need here + defp get_attachment_filepath(note, uploads_dir) do + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = note + filename = href |> Path.basename() + "#{uploads_dir}/#{filename}" + end + test "returns an object by it's AP id" do object = insert(:note) found_object = Object.get_by_ap_id(object.data["id"]) @@ -95,14 +102,13 @@ test "Disabled via config" do {:ok, %Object{} = attachment} = Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - path = href |> Path.dirname() |> Path.basename() + path = get_attachment_filepath(note, uploads_dir) - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + assert File.exists?("#{path}") Object.delete(note) @@ -111,7 +117,7 @@ test "Disabled via config" do assert Object.get_by_id(note.id).data["deleted"] refute Object.get_by_id(attachment.id) == nil - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + assert File.exists?("#{path}") end test "in subdirectories" do @@ -129,14 +135,13 @@ test "in subdirectories" do {:ok, %Object{} = attachment} = Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - path = href |> Path.dirname() |> Path.basename() + path = get_attachment_filepath(note, uploads_dir) - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + assert File.exists?("#{path}") Object.delete(note) @@ -145,7 +150,7 @@ test "in subdirectories" do assert Object.get_by_id(note.id).data["deleted"] assert Object.get_by_id(attachment.id) == nil - assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") + refute File.exists?("#{path}") end test "with dedupe enabled" do @@ -168,13 +173,11 @@ test "with dedupe enabled" do {:ok, %Object{} = attachment} = Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - filename = Path.basename(href) + path = get_attachment_filepath(note, uploads_dir) - assert {:ok, files} = File.ls(uploads_dir) - assert filename in files + assert File.exists?("#{path}") Object.delete(note) @@ -182,8 +185,8 @@ test "with dedupe enabled" do assert Object.get_by_id(note.id).data["deleted"] assert Object.get_by_id(attachment.id) == nil - assert {:ok, files} = File.ls(uploads_dir) - refute filename in files + # what if another test runs concurrently using the same image file? + refute File.exists?("#{path}") end test "with objects that have legacy data.url attribute" do @@ -203,14 +206,13 @@ test "with objects that have legacy data.url attribute" do {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id}) - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - path = href |> Path.dirname() |> Path.basename() + path = get_attachment_filepath(note, uploads_dir) - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + assert File.exists?("#{path}") Object.delete(note) @@ -219,7 +221,7 @@ test "with objects that have legacy data.url attribute" do assert Object.get_by_id(note.id).data["deleted"] assert Object.get_by_id(attachment.id) == nil - assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") + refute File.exists?("#{path}") end test "With custom base_url" do @@ -238,14 +240,13 @@ test "With custom base_url" do {:ok, %Object{} = attachment} = Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - path = href |> Path.dirname() |> Path.basename() + path = get_attachment_filepath(note, uploads_dir) - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + assert File.exists?("#{path}") Object.delete(note) @@ -254,7 +255,7 @@ test "With custom base_url" do assert Object.get_by_id(note.id).data["deleted"] assert Object.get_by_id(attachment.id) == nil - assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") + refute File.exists?("#{path}") end end diff --git a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs index a7d4d493c..073e70705 100644 --- a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs +++ b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs @@ -29,13 +29,13 @@ test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: mi %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - assert new_opts == [ + assert Keyword.equal?(new_opts, class: false, extra: true, new_window: false, rel: "testing", strip_prefix: false - ] + ) clear_config(Pleroma.Formatter, new_opts) assert new_opts == Pleroma.Config.get(Pleroma.Formatter) @@ -67,6 +67,6 @@ test "transform_opts/1 returns a list of compatible opts", %{migration: migratio strip_prefix: false ] - assert migration.transform_opts(old_opts) == expected_opts + assert Keyword.equal?(migration.transform_opts(old_opts), expected_opts) end end diff --git a/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs b/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs index 65c9961b0..9f589882d 100644 --- a/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs +++ b/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs @@ -26,16 +26,16 @@ test "change/0 converts a map into a list", %{migration: migration} do %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - assert new_opts == [ + assert Keyword.equal?(new_opts, class: false, extra: true, new_window: false, rel: "F", strip_prefix: false - ] + ) clear_config(Pleroma.Formatter, new_opts) - assert new_opts == Pleroma.Config.get(Pleroma.Formatter) + assert Keyword.equal?(new_opts, Pleroma.Config.get(Pleroma.Formatter)) {text, _mentions, []} = Pleroma.Formatter.linkify( @@ -61,7 +61,7 @@ test "change/0 skips if Pleroma.Formatter config is already a list", %{migration %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - assert new_opts == opts + assert Keyword.equal?(new_opts, opts) end test "change/0 skips if Pleroma.Formatter is empty", %{migration: migration} do diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs index e3e2a1571..fc6ae42bc 100644 --- a/test/pleroma/reverse_proxy_test.exs +++ b/test/pleroma/reverse_proxy_test.exs @@ -75,13 +75,16 @@ test "common", %{conn: conn} do Tesla.Mock.mock(fn %{method: :head, url: "/head"} -> %Tesla.Env{ status: 200, - headers: [{"content-type", "text/html; charset=utf-8"}], + headers: [{"content-type", "image/png"}], body: "" } end) conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head") - assert html_response(conn, 200) == "" + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["image/png"] + assert conn.resp_body == "" end end @@ -252,4 +255,38 @@ test "with content-disposition header", %{conn: conn} do assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers end end + + describe "content-type sanitisation" do + test "preserves video type", %{conn: conn} do + Tesla.Mock.mock(fn %{method: :get, url: "/content"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "video/mp4"}], + body: "test" + } + end) + + conn = ReverseProxy.call(Map.put(conn, :method, "GET"), "/content") + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["video/mp4"] + assert conn.resp_body == "test" + end + + test "replaces application type", %{conn: conn} do + Tesla.Mock.mock(fn %{method: :get, url: "/content"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: "test" + } + end) + + conn = ReverseProxy.call(Map.put(conn, :method, "GET"), "/content") + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] + assert conn.resp_body == "test" + end + end end diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs index 1f52484a5..e2d02fe4c 100644 --- a/test/pleroma/signature_test.exs +++ b/test/pleroma/signature_test.exs @@ -71,6 +71,35 @@ test "it returns error when not found user" do end end + defp split_signature(sig) do + sig + |> String.split(",") + |> Enum.map(fn part -> + [key, value] = String.split(part, "=", parts: 2) + [key, String.trim(value, ~s|"|)] + end) + |> Enum.sort_by(fn [k, _] -> k end) + end + + # Break up a signature and check by parts + defp assert_signature_equal(sig_a, sig_b) when is_binary(sig_a) and is_binary(sig_b) do + parts_a = split_signature(sig_a) + parts_b = split_signature(sig_b) + + parts_a + |> Enum.with_index() + |> Enum.each(fn {part_a, index} -> + part_b = Enum.at(parts_b, index) + assert_part_equal(part_a, part_b) + end) + end + + defp assert_part_equal(part_a, part_b) do + if part_a != part_b do + flunk("Signature check failed - expected #{part_a} to equal #{part_b}") + end + end + describe "sign/2" do test "it returns signature headers" do user = @@ -79,14 +108,18 @@ test "it returns signature headers" do keys: @private_key }) - assert Signature.sign( - user, - %{ - host: "test.test", - "content-length": 100 - } - ) == - "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\"" + headers = %{ + host: "test.test", + "content-length": 100 + } + + assert_signature_equal( + Signature.sign( + user, + headers + ), + "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\"" + ) end test "it returns error" do diff --git a/test/pleroma/upload_test.exs b/test/pleroma/upload_test.exs index ad6065b43..27a2d1b97 100644 --- a/test/pleroma/upload_test.exs +++ b/test/pleroma/upload_test.exs @@ -188,7 +188,7 @@ test "copies the file to the configured folder with anonymizing filename" do refute data["name"] == "an [image.jpg" end - test "escapes invalid characters in url" do + test "mangles the filename" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ @@ -200,23 +200,8 @@ test "escapes invalid characters in url" do {:ok, data} = Upload.store(file) [attachment_url | _] = data["url"] - assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg" - end - - test "escapes reserved uri characters" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpeg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: ":?#[]@!$&\\'()*+,;=.jpg" - } - - {:ok, data} = Upload.store(file) - [attachment_url | _] = data["url"] - - assert Path.basename(attachment_url["href"]) == - "%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg" + refute Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg" + refute Path.basename(attachment_url["href"]) == "an… image.jpg" end end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index c33528a67..96ca8d0fd 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -326,9 +326,9 @@ test "unfollow with synchronizes external user" do insert(:user, %{ local: false, nickname: "fuser2", - ap_id: "http://localhost:4001/users/fuser2", - follower_address: "http://localhost:4001/users/fuser2/followers", - following_address: "http://localhost:4001/users/fuser2/following" + ap_id: "http://remote.org/users/fuser2", + follower_address: "http://remote.org/users/fuser2/followers", + following_address: "http://remote.org/users/fuser2/following" }) {:ok, user, followed} = User.follow(user, followed, :follow_accept) @@ -1004,23 +1004,13 @@ test "it doesn't fail on invalid alsoKnownAs entries" do test "returns an ap_id for a user" do user = insert(:user) - assert User.ap_id(user) == - Pleroma.Web.Router.Helpers.user_feed_url( - Pleroma.Web.Endpoint, - :feed_redirect, - user.nickname - ) + assert User.ap_id(user) == url(@endpoint, ~p[/users/#{user.nickname}]) end test "returns an ap_followers link for a user" do user = insert(:user) - assert User.ap_followers(user) == - Pleroma.Web.Router.Helpers.user_feed_url( - Pleroma.Web.Endpoint, - :feed_redirect, - user.nickname - ) <> "/followers" + assert User.ap_followers(user) == url(@endpoint, ~p[/users/#{user.nickname}/followers]) end describe "remote user changeset" do @@ -1363,25 +1353,27 @@ test "does not block domain with same end if wildcard added" do refute User.blocks?(user, collateral_user) end - test "blocks domain with wildcard for subdomain" do - user = insert(:user) - - user_from_subdomain = - insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"}) - - user_with_two_subdomains = - insert(:user, %{ - ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully" - }) - - user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") - - assert User.blocks?(user, user_from_subdomain) - assert User.blocks?(user, user_with_two_subdomains) - assert User.blocks?(user, user_domain) - end + # This behaviour is not honoured by the timeline query + # re-add at a later date when UX is established + # test "blocks domain with wildcard for subdomain" do + # user = insert(:user) + # + # user_from_subdomain = + # insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"}) + # + # user_with_two_subdomains = + # insert(:user, %{ + # ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully" + # }) + # + # user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) + # + # {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") + # + # assert User.blocks?(user, user_from_subdomain) + # assert User.blocks?(user, user_with_two_subdomains) + # assert User.blocks?(user, user_domain) + # end test "unblocks domains" do user = insert(:user) @@ -2185,8 +2177,8 @@ test "it returns a list of AP ids for a given set of nicknames" do describe "sync followers count" do setup do - user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed") - user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2") + user1 = insert(:user, local: false, ap_id: "http://remote.org/users/masto_closed") + user2 = insert(:user, local: false, ap_id: "http://remote.org/users/fuser2") insert(:user, local: true) insert(:user, local: false, is_active: false) {:ok, user1: user1, user2: user2} @@ -2280,8 +2272,8 @@ test "updates the counters normally on following/getting a follow when disabled" other_user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", + follower_address: "http://remote.org/users/masto_closed/followers", + following_address: "http://remote.org/users/masto_closed/following", ap_enabled: true ) @@ -2302,8 +2294,8 @@ test "synchronizes the counters with the remote instance for the followed when e other_user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", + follower_address: "http://remote.org/users/masto_closed/followers", + following_address: "http://remote.org/users/masto_closed/following", ap_enabled: true ) @@ -2324,8 +2316,8 @@ test "synchronizes the counters with the remote instance for the follower when e other_user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", + follower_address: "http://remote.org/users/masto_closed/followers", + following_address: "http://remote.org/users/masto_closed/following", ap_enabled: true ) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 4ef0c9302..dcb5f143c 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -39,7 +39,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do test "with the relay active, it returns the relay user", %{conn: conn} do res = conn - |> get(activity_pub_path(conn, :relay)) + |> get(~p"/relay") |> json_response(200) assert res["id"] =~ "/relay" @@ -49,7 +49,7 @@ test "with the relay disabled, it returns 404", %{conn: conn} do clear_config([:instance, :allow_relay], false) conn - |> get(activity_pub_path(conn, :relay)) + |> get(~p"/relay") |> json_response(404) end @@ -59,7 +59,7 @@ test "on non-federating instance, it returns 404", %{conn: conn} do conn |> assign(:user, user) - |> get(activity_pub_path(conn, :relay)) + |> get(~p"/relay") |> json_response(404) end end @@ -68,7 +68,7 @@ test "on non-federating instance, it returns 404", %{conn: conn} do test "it returns the internal fetch user", %{conn: conn} do res = conn - |> get(activity_pub_path(conn, :internal_fetch)) + |> get(~p"/internal/fetch") |> json_response(200) assert res["id"] =~ "/fetch" @@ -80,7 +80,7 @@ test "on non-federating instance, it returns 404", %{conn: conn} do conn |> assign(:user, user) - |> get(activity_pub_path(conn, :internal_fetch)) + |> get(~p"/internal/fetch") |> json_response(404) end end diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index d4d1c2aac..5ad6d4716 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -312,7 +312,7 @@ test "fetches user featured collection" do end test "fetches user featured collection using the first property" do - featured_url = "https://friendica.example.com/raha/collections/featured" + featured_url = "https://friendica.example.com/featured/raha" first_url = "https://friendica.example.com/featured/raha?page=1" featured_data = @@ -350,7 +350,7 @@ test "fetches user featured collection using the first property" do end test "fetches user featured when it has string IDs" do - featured_url = "https://example.com/alisaie/collections/featured" + featured_url = "https://example.com/users/alisaie/collections/featured" dead_url = "https://example.com/users/alisaie/statuses/108311386746229284" featured_data = @@ -1304,14 +1304,6 @@ test "returns reblogs for users for whom reblogs have not been muted" do %{test_file: test_file} end - test "strips / from filename", %{test_file: file} do - file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"} - {:ok, %Object{} = object} = ActivityPub.upload(file) - [%{"href" => href}] = object.data["url"] - assert Regex.match?(~r"/bad.jpg$", href) - refute Regex.match?(~r"/nested/", href) - end - test "sets a description if given", %{test_file: file} do {:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file") assert object.data["name"] == "a cool file" @@ -1651,8 +1643,8 @@ test "synchronizes following/followers counters" do user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/fuser2/followers", - following_address: "http://localhost:4001/users/fuser2/following" + follower_address: "http://remote.org/users/fuser2/followers", + following_address: "http://remote.org/users/fuser2/following" ) {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) @@ -1663,7 +1655,7 @@ test "synchronizes following/followers counters" do test "detects hidden followers" do mock(fn env -> case env.url do - "http://localhost:4001/users/masto_closed/followers?page=1" -> + "http://remote.org/users/masto_closed/followers?page=1" -> %Tesla.Env{status: 403, body: ""} _ -> @@ -1674,8 +1666,8 @@ test "detects hidden followers" do user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" + follower_address: "http://remote.org/users/masto_closed/followers", + following_address: "http://remote.org/users/masto_closed/following" ) {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) @@ -1686,7 +1678,7 @@ test "detects hidden followers" do test "detects hidden follows" do mock(fn env -> case env.url do - "http://localhost:4001/users/masto_closed/following?page=1" -> + "http://remote.org/users/masto_closed/following?page=1" -> %Tesla.Env{status: 403, body: ""} _ -> @@ -1697,8 +1689,8 @@ test "detects hidden follows" do user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" + follower_address: "http://remote.org/users/masto_closed/followers", + following_address: "http://remote.org/users/masto_closed/following" ) {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) @@ -1710,8 +1702,8 @@ test "detects hidden follows/followers for friendica" do user = insert(:user, local: false, - follower_address: "http://localhost:8080/followers/fuser3", - following_address: "http://localhost:8080/following/fuser3" + follower_address: "http://remote.org/followers/fuser3", + following_address: "http://remote.org/following/fuser3" ) {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) @@ -1724,28 +1716,28 @@ test "detects hidden follows/followers for friendica" do test "doesn't crash when follower and following counters are hidden" do mock(fn env -> case env.url do - "http://localhost:4001/users/masto_hidden_counters/following" -> + "http://remote.org/users/masto_hidden_counters/following" -> json( %{ "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/followers" + "id" => "http://remote.org/users/masto_hidden_counters/following" }, headers: HttpRequestMock.activitypub_object_headers() ) - "http://localhost:4001/users/masto_hidden_counters/following?page=1" -> + "http://remote.org/users/masto_hidden_counters/following?page=1" -> %Tesla.Env{status: 403, body: ""} - "http://localhost:4001/users/masto_hidden_counters/followers" -> + "http://remote.org/users/masto_hidden_counters/followers" -> json( %{ "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/following" + "id" => "http://remote.org/users/masto_hidden_counters/followers" }, headers: HttpRequestMock.activitypub_object_headers() ) - "http://localhost:4001/users/masto_hidden_counters/followers?page=1" -> + "http://remote.org/users/masto_hidden_counters/followers?page=1" -> %Tesla.Env{status: 403, body: ""} end end) @@ -1753,8 +1745,8 @@ test "doesn't crash when follower and following counters are hidden" do user = insert(:user, local: false, - follower_address: "http://localhost:4001/users/masto_hidden_counters/followers", - following_address: "http://localhost:4001/users/masto_hidden_counters/following" + follower_address: "http://remote.org/users/masto_hidden_counters/followers", + following_address: "http://remote.org/users/masto_hidden_counters/following" ) {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) @@ -2629,6 +2621,14 @@ test "allow fetching of accounts with an empty string name field" do assert user.name == " " end + test "pin_data_from_featured_collection will ignore unsupported values" do + assert %{} == + ActivityPub.pin_data_from_featured_collection(%{ + "type" => "CollectionThatIsNotRealAndCannotHurtMe", + "first" => "https://social.example/users/alice/collections/featured?page=true" + }) + end + describe "persist/1" do test "should not persist remote delete activities" do poster = insert(:user, local: false) diff --git a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs index 875cf8f43..1ae42036d 100644 --- a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do accept: [], avatar_removal: [], banner_removal: [], + background_removal: [], reject_deletes: [] ) @@ -283,7 +284,7 @@ test "obfuscates domains listed in :transparency_obfuscate_domains" do assert {:ok, %{ - mrf_simple: %{reject: ["rem***.*****nce", "a.b"]}, + mrf_simple: %{reject: ["rem***.*****nce", "*.b"]}, mrf_simple_info: %{reject: %{"rem***.*****nce" => %{}}} }} = SimplePolicy.describe() end @@ -618,6 +619,42 @@ test "match with wildcard domain" do end end + describe "when :background_removal" do + test "is empty" do + clear_config([:mrf_simple, :background_removal], []) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "is not empty but it doesn't have a matching host" do + clear_config([:mrf_simple, :background_removal], [{"non.matching.remote", ""}]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "has a matching host" do + clear_config([:mrf_simple, :background_removal], [{"remote.instance", ""}]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["backgroundUrl"] + end + + test "match with wildcard domain" do + clear_config([:mrf_simple, :background_removal], [{"*.remote.instance", ""}]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["backgroundUrl"] + end + end + describe "when :reject_deletes is empty" do setup do: clear_config([:mrf_simple, :reject_deletes], []) @@ -701,6 +738,10 @@ defp build_remote_user do "url" => "http://example.com/image.jpg", "type" => "Image" }, + "backgroundUrl" => %{ + "url" => "http://example.com/background.jpg", + "type" => "Image" + }, "type" => "Person" } end diff --git a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs index b0a7e8993..ba5087f1b 100644 --- a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs @@ -7,9 +7,57 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do alias Pleroma.Config alias Pleroma.Emoji + alias Pleroma.Emoji.Pack alias Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy + defp has_pack?() do + case Pack.load_pack("stolen") do + {:ok, _pack} -> true + {:error, :enoent} -> false + end + end + + defp has_emoji?(shortcode) do + case Pack.load_pack("stolen") do + {:ok, pack} -> Map.has_key?(pack.files, shortcode) + {:error, :enoent} -> false + end + end + + defmacro mock_tesla( + url \\ "https://example.org/emoji/firedfox.png", + status \\ 200, + headers \\ [], + get_body \\ File.read!("test/fixtures/image.jpg") + ) do + quote do + Tesla.Mock.mock(fn + %{method: :head, url: unquote(url)} -> + %Tesla.Env{ + status: unquote(status), + body: nil, + url: unquote(url), + headers: unquote(headers) + } + + %{method: :get, url: unquote(url)} -> + %Tesla.Env{ + status: unquote(status), + body: unquote(get_body), + url: unquote(url), + headers: unquote(headers) + } + end) + end + end + setup do + clear_config(:mrf_steal_emoji, + hosts: ["example.org"], + size_limit: 284_468, + download_unknown_size: true + ) + emoji_path = [:instance, :static_dir] |> Config.get() |> Path.join("emoji/stolen") Emoji.reload() @@ -26,48 +74,75 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do File.rm_rf!(emoji_path) end) - [message: message, path: emoji_path] + [message: message] end test "does nothing by default", %{message: message} do refute "firedfox" in installed() + clear_config(:mrf_steal_emoji, []) assert {:ok, _message} = StealEmojiPolicy.filter(message) refute "firedfox" in installed() end test "Steals emoji on unknown shortcode from allowed remote host", %{ - message: message, - path: path + message: message } do refute "firedfox" in installed() - refute File.exists?(path) + refute has_pack?() - Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.png"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} - end) - - clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468) + mock_tesla() assert {:ok, _message} = StealEmojiPolicy.filter(message) assert "firedfox" in installed() - assert File.exists?(path) + assert has_pack?() - assert path - |> Path.join("firedfox.png") - |> File.exists?() + assert has_emoji?("firedfox") + end + + test "rejects invalid shortcodes" do + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"fired/fox", "https://example.org/emoji/firedfox"}], + "actor" => "https://example.org/users/admin" + } + } + + mock_tesla() + + refute "firedfox" in installed() + refute has_pack?() + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + refute "fired/fox" in installed() + refute has_emoji?("fired/fox") + end + + test "prefers content-type header for extension" do + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.fud"}], + "actor" => "https://example.org/users/admin" + } + } + + mock_tesla("https://example.org/emoji/firedfox.fud", 200, [{"content-type", "image/gif"}]) + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + assert "firedfox" in installed() + assert has_emoji?("firedfox") end test "reject regex shortcode", %{message: message} do refute "firedfox" in installed() - clear_config(:mrf_steal_emoji, - hosts: ["example.org"], - size_limit: 284_468, - rejected_shortcodes: [~r/firedfox/] - ) + clear_config([:mrf_steal_emoji, :rejected_shortcodes], [~r/firedfox/]) assert {:ok, _message} = StealEmojiPolicy.filter(message) @@ -77,11 +152,7 @@ test "reject regex shortcode", %{message: message} do test "reject string shortcode", %{message: message} do refute "firedfox" in installed() - clear_config(:mrf_steal_emoji, - hosts: ["example.org"], - size_limit: 284_468, - rejected_shortcodes: ["firedfox"] - ) + clear_config([:mrf_steal_emoji, :rejected_shortcodes], ["firedfox"]) assert {:ok, _message} = StealEmojiPolicy.filter(message) @@ -91,11 +162,9 @@ test "reject string shortcode", %{message: message} do test "reject if size is above the limit", %{message: message} do refute "firedfox" in installed() - Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.png"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} - end) + mock_tesla() - clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 50_000) + clear_config([:mrf_steal_emoji, :size_limit], 50_000) assert {:ok, _message} = StealEmojiPolicy.filter(message) @@ -105,11 +174,7 @@ test "reject if size is above the limit", %{message: message} do test "reject if host returns error", %{message: message} do refute "firedfox" in installed() - Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.png"} -> - {:ok, %Tesla.Env{status: 404, body: "Not found"}} - end) - - clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468) + mock_tesla("https://example.org/emoji/firedfox.png", 404, [], "Not found") ExUnit.CaptureLog.capture_log(fn -> assert {:ok, _message} = StealEmojiPolicy.filter(message) @@ -118,5 +183,44 @@ test "reject if host returns error", %{message: message} do refute "firedfox" in installed() end + test "reject unknown size", %{message: message} do + clear_config([:mrf_steal_emoji, :download_unknown_size], false) + mock_tesla() + + refute "firedfox" in installed() + + ExUnit.CaptureLog.capture_log(fn -> + assert {:ok, _message} = StealEmojiPolicy.filter(message) + end) =~ + "MRF.StealEmojiPolicy: Failed to fetch https://example.org/emoji/firedfox.png: {:remote_size, false}" + + refute "firedfox" in installed() + end + + test "reject too large content-size before download", %{message: message} do + clear_config([:mrf_steal_emoji, :download_unknown_size], false) + mock_tesla("https://example.org/emoji/firedfox.png", 200, [{"content-length", 2 ** 30}]) + + refute "firedfox" in installed() + + ExUnit.CaptureLog.capture_log(fn -> + assert {:ok, _message} = StealEmojiPolicy.filter(message) + end) =~ + "MRF.StealEmojiPolicy: Failed to fetch https://example.org/emoji/firedfox.png: {:remote_size, false}" + + refute "firedfox" in installed() + end + + test "accepts content-size below limit", %{message: message} do + clear_config([:mrf_steal_emoji, :download_unknown_size], false) + mock_tesla("https://example.org/emoji/firedfox.png", 200, [{"content-length", 2}]) + + refute "firedfox" in installed() + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + assert "firedfox" in installed() + end + defp installed, do: Emoji.get_all() |> Enum.map(fn {k, _} -> k end) end diff --git a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs index 9150b8d41..f8dec09d3 100644 --- a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs @@ -11,6 +11,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do import Pleroma.Factory describe "attachments" do + test "works with apng" do + attachment = + %{ + "mediaType" => "image/apng", + "name" => "", + "type" => "Document", + "url" => + "https://media.misskeyusercontent.com/io/2859c26e-cd43-4550-848b-b6243bc3fe28.apng" + } + + assert {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "image/apng" + end + test "works with honkerific attachments" do attachment = %{ "mediaType" => "", diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index 80714b1db..28a591d3c 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -155,7 +155,13 @@ test "it blocks but does not unfollow if the relevant setting is set", %{ user = insert(:user, local: false) {:ok, update_data, []} = - Builder.update(user, %{"id" => user.ap_id, "type" => "Person", "name" => "new name!"}) + Builder.update(user, %{ + "id" => user.ap_id, + "type" => "Person", + "name" => "new name!", + "icon" => %{"type" => "Image", "url" => "https://example.org/icon.png"}, + "backgroundUrl" => %{"type" => "Image", "url" => "https://example.org/bg.jxl"} + }) {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) @@ -165,7 +171,10 @@ test "it blocks but does not unfollow if the relevant setting is set", %{ test "it updates the user", %{user: user, update: update} do {:ok, _, _} = SideEffects.handle(update) user = User.get_by_id(user.id) + assert user.name == "new name!" + assert [%{"href" => "https://example.org/icon.png"}] = user.avatar["url"] + assert [%{"href" => "https://example.org/bg.jxl"}] = user.background["url"] end test "it uses a given changeset to update", %{user: user, update: update} do diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs index 5501e64d6..abe91cdea 100644 --- a/test/pleroma/web/activity_pub/views/user_view_test.exs +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -58,16 +58,19 @@ test "Does not add an avatar image if the user hasn't set one" do result = UserView.render("user.json", %{user: user}) refute result["icon"] refute result["image"] + refute result["backgroundUrl"] user = insert(:user, avatar: %{"url" => [%{"href" => "https://someurl"}]}, - banner: %{"url" => [%{"href" => "https://somebanner"}]} + banner: %{"url" => [%{"href" => "https://somebanner"}]}, + background: %{"url" => [%{"href" => "https://somebackground"}]} ) result = UserView.render("user.json", %{user: user}) assert result["icon"]["url"] == "https://someurl" assert result["image"]["url"] == "https://somebanner" + assert result["backgroundUrl"]["url"] == "https://somebackground" end test "renders an invisible user with the invisible property set to true" do @@ -76,6 +79,15 @@ test "renders an invisible user with the invisible property set to true" do assert %{"invisible" => true} = UserView.render("service.json", %{user: user}) end + test "service has a few essential fields" do + user = insert(:user) + result = UserView.render("service.json", %{user: user}) + assert result["id"] + assert result["type"] == "Application" + assert result["inbox"] + assert result["outbox"] + end + test "renders AKAs" do akas = ["https://i.tusooa.xyz/users/test-pleroma"] user = insert(:user, also_known_as: akas) diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs index e0a2cb9de..68d77ae5a 100644 --- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs @@ -783,7 +783,8 @@ test "it confirms emails of two users", %{conn: conn, admin: admin} do describe "PATCH /resend_confirmation_email" do test "it resend emails for two users", %{conn: conn, admin: admin} do - [first_user, second_user] = insert_pair(:user, is_confirmed: false) + [first_user, second_user] = + insert_pair(:user, is_confirmed: false, confirmation_token: "something") ret_conn = patch(conn, "/api/v1/pleroma/admin/users/resend_confirmation_email", %{ diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs index 2ec9ca969..ace71785b 100644 --- a/test/pleroma/web/admin_api/controllers/report_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs @@ -212,7 +212,7 @@ test "updates state of multiple reports", %{ test "returns empty response when no reports created", %{conn: conn} do response = conn - |> get(report_path(conn, :index)) + |> get(~p"/api/v1/pleroma/admin/reports") |> json_response_and_validate_schema(:ok) assert Enum.empty?(response["reports"]) @@ -232,7 +232,7 @@ test "returns reports", %{conn: conn} do response = conn - |> get(report_path(conn, :index)) + |> get(~p"/api/v1/pleroma/admin/reports") |> json_response_and_validate_schema(:ok) [report] = response["reports"] @@ -264,7 +264,7 @@ test "returns reports with specified state", %{conn: conn} do response = conn - |> get(report_path(conn, :index, %{state: "open"})) + |> get(~p[/api/v1/pleroma/admin/reports?#{[state: "open"]}]) |> json_response_and_validate_schema(:ok) assert [open_report] = response["reports"] @@ -276,7 +276,7 @@ test "returns reports with specified state", %{conn: conn} do response = conn - |> get(report_path(conn, :index, %{state: "closed"})) + |> get(~p[/api/v1/pleroma/admin/reports?#{[state: "closed"]}]) |> json_response_and_validate_schema(:ok) assert [closed_report] = response["reports"] @@ -288,7 +288,7 @@ test "returns reports with specified state", %{conn: conn} do assert %{"total" => 0, "reports" => []} == conn - |> get(report_path(conn, :index, %{state: "resolved"})) + |> get(~p[/api/v1/pleroma/admin/reports?#{[state: "resolved"]}]) |> json_response_and_validate_schema(:ok) end diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs index ba6465630..6dcef1a39 100644 --- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs @@ -685,7 +685,7 @@ test "load users with actor_type is Person", %{admin: admin, conn: conn} do response = conn - |> get(user_path(conn, :index), %{actor_types: ["Person"]}) + |> get(~p"/api/v1/pleroma/admin/users", %{actor_types: ["Person"]}) |> json_response_and_validate_schema(200) users = [ @@ -706,7 +706,7 @@ test "load users with actor_type is Person and Service", %{admin: admin, conn: c response = conn - |> get(user_path(conn, :index), %{actor_types: ["Person", "Service"]}) + |> get(~p"/api/v1/pleroma/admin/users", %{actor_types: ["Person", "Service"]}) |> json_response_and_validate_schema(200) users = [ @@ -727,7 +727,7 @@ test "load users with actor_type is Service", %{conn: conn} do response = conn - |> get(user_path(conn, :index), %{actor_types: ["Service"]}) + |> get(~p"/api/v1/pleroma/admin/users", %{actor_types: ["Service"]}) |> json_response_and_validate_schema(200) users = [user_response(user_service, %{"actor_type" => "Service"})] diff --git a/test/pleroma/web/akkoma_api/frontend_settings_controller_test.exs b/test/pleroma/web/akkoma_api/frontend_settings_controller_test.exs index 07e45d9b9..7b1329593 100644 --- a/test/pleroma/web/akkoma_api/frontend_settings_controller_test.exs +++ b/test/pleroma/web/akkoma_api/frontend_settings_controller_test.exs @@ -119,4 +119,18 @@ test "deletes a config" do ) == nil end end + + describe "PUT /api/v1/akkoma/preferred_frontend" do + test "sets a cookie with selected frontend" do + %{conn: conn} = oauth_access(["read"]) + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/akkoma/preferred_frontend", %{"frontend_name" => "pleroma-fe/stable"}) + + json_response_and_validate_schema(response, 200) + assert %{"preferred_frontend" => %{value: "pleroma-fe/stable"}} = response.resp_cookies + end + end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index f2675ad01..379cf85b8 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -1061,6 +1061,36 @@ test "directly follows a non-locked local user" do assert User.following?(follower, followed) end + + test "directly follows back a locked, but followback-allowing local user" do + uopen = insert(:user, is_locked: false) + uselective = insert(:user, is_locked: true, permit_followback: true) + + assert {:ok, uselective, uopen, %{data: %{"state" => "accept"}}} = + CommonAPI.follow(uselective, uopen) + + assert User.get_follow_state(uselective, uopen) == :follow_accept + + assert {:ok, uopen, uselective, %{data: %{"state" => "accept"}}} = + CommonAPI.follow(uopen, uselective) + + assert User.get_follow_state(uopen, uselective) == :follow_accept + end + + test "creates a pending request for locked, non-followback local user" do + uopen = insert(:user, is_locked: false) + ulocked = insert(:user, is_locked: true, permit_followback: false) + + assert {:ok, ulocked, uopen, %{data: %{"state" => "accept"}}} = + CommonAPI.follow(ulocked, uopen) + + assert User.get_follow_state(ulocked, uopen) == :follow_accept + + assert {:ok, uopen, ulocked, %{data: %{"state" => "pending"}}} = + CommonAPI.follow(uopen, ulocked) + + assert User.get_follow_state(uopen, ulocked) == :follow_pending + end end describe "unfollow/2" do diff --git a/test/pleroma/web/feed/tag_controller_test.exs b/test/pleroma/web/feed/tag_controller_test.exs index fe7940f41..f67d5485d 100644 --- a/test/pleroma/web/feed/tag_controller_test.exs +++ b/test/pleroma/web/feed/tag_controller_test.exs @@ -50,7 +50,7 @@ test "gets a feed (ATOM)", %{conn: conn} do response = conn |> put_req_header("accept", "application/atom+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart.atom")) + |> get(~p"/tags/pleromaart.atom") |> response(200) xml = parse(response) @@ -117,7 +117,7 @@ test "gets a feed (RSS)", %{conn: conn} do response = conn |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) + |> get(~p"/tags/pleromaart.rss") |> response(200) xml = parse(response) @@ -157,7 +157,7 @@ test "gets a feed (RSS)", %{conn: conn} do response = conn |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) + |> get(~p"/tags/pleromaart.rss") |> response(200) xml = parse(response) @@ -188,7 +188,7 @@ test "gets a feed (RSS)", %{conn: conn} do test "returns 404 for tags feed", %{conn: conn} do conn |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) + |> get(~p"/tags/pleromaart.rss") |> response(404) end end diff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs index 245ffcf0a..d5d9faf06 100644 --- a/test/pleroma/web/feed/user_controller_test.exs +++ b/test/pleroma/web/feed/user_controller_test.exs @@ -66,7 +66,7 @@ test "gets an atom feed", %{conn: conn, user: user, object: object, max_id: max_ resp = conn |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) + |> get(~p[/users/#{user.nickname}/feed]) |> response(200) activity_titles = @@ -128,7 +128,7 @@ test "returns 404 for a missing feed", %{conn: conn} do conn = conn |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, "nonexisting")) + |> get(~p"/users/nonexisting/feed") assert response(conn, 404) end @@ -144,7 +144,7 @@ test "returns feed with public and unlisted activities", %{conn: conn} do resp = conn |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) + |> get(~p[/users/#{user.nickname}/feed]) |> response(200) activity_titles = @@ -163,7 +163,7 @@ test "returns 404 when the user is remote", %{conn: conn} do assert conn |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) + |> get(~p[/users/#{user.nickname}/feed]) |> response(404) end @@ -240,7 +240,7 @@ test "with non-html / non-json format, it returns error when user is not found", response = conn |> put_req_header("accept", "application/xml") - |> get(user_feed_path(conn, :feed, "jimm")) + |> get(~p"/users/jimm/feed") |> response(404) assert response == ~S({"error":"Not found"}) @@ -258,7 +258,7 @@ test "returns 404 for user feed", %{conn: conn} do assert conn |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) + |> get(~p[/users/#{user.nickname}/feed]) |> response(404) end end diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 6ca9cfc50..ede14030b 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -2060,4 +2060,32 @@ test "removing user from followers errors", %{user: user, conn: conn} do assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) end end + + describe "preferences" do + test "get account preferences" do + user = insert(:user, default_scope: "public") + %{conn: conn} = oauth_access(["read:accounts"], user: user) + + conn = get(conn, "/api/v1/preferences") + response = json_response_and_validate_schema(conn, 200) + + assert %{ + "posting:default:language" => nil, + "posting:default:sensitive" => false, + "posting:default:visibility" => "public", + "reading:expand:media" => "default", + "reading:expand:spoilers" => false + } = response + end + + test "test changing account preferences" do + user = insert(:user, default_scope: "unlisted") + %{conn: conn} = oauth_access(["read:accounts"], user: user) + + conn = get(conn, "/api/v1/preferences") + response = json_response_and_validate_schema(conn, 200) + + assert response["posting:default:visibility"] == "unlisted" + end + end end diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index bd3735925..25a18e145 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -1883,7 +1883,7 @@ test "favorites paginate correctly" do # Using the header for pagination works correctly [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ") - [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next) + [_, max_id] = Regex.run(~r/max_id=([^&>]+)/, next) assert max_id == third_favorite.id diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index a347c7987..92a5726bf 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -91,7 +91,7 @@ test "updates the user's bio", %{conn: conn} do assert user_data = json_response_and_validate_schema(conn, 200) assert user_data["note"] == - ~s(I drink #cofe with @#{user2.nickname}

    suya..) + ~s(I drink #cofe with @#{user2.nickname}

    suya..) assert user_data["source"]["note"] == raw_bio @@ -529,7 +529,7 @@ test "update fields with a link to content with rel=me, with frontend path", %{ user: user, conn: conn } do - fe_url = "#{Pleroma.Web.Endpoint.url()}/#{user.nickname}" + fe_url = url(~p[/#{user.nickname}]) Tesla.Mock.mock(fn %{url: "http://example.com/rel_me/fe_path"} -> diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index 5b8aea8ac..d017b87c8 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -40,7 +40,8 @@ test "Represent a user account" do emoji: %{"karjalanpiirakka" => "/file.png"}, raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"", also_known_as: ["https://shitposter.zone/users/shp"], - status_ttl_days: 5 + status_ttl_days: 5, + last_status_at: ~N[2023-12-31T15:06:17] }) insert(:instance, %{host: "example.com", nodeinfo: %{version: "2.1"}}) @@ -65,7 +66,8 @@ test "Represent a user account" do }, favicon: nil }, - status_ttl_days: 5 + status_ttl_days: 5, + permit_followback: false }, avatar: "http://localhost:4001/images/avi.png", avatar_static: "http://localhost:4001/images/avi.png", @@ -91,7 +93,7 @@ test "Represent a user account" do fields: [] }, fqn: "shp@shitposter.club", - last_status_at: nil, + last_status_at: ~D[2023-12-31], pleroma: %{ ap_id: user.ap_id, also_known_as: ["https://shitposter.zone/users/shp"], @@ -248,7 +250,8 @@ test "Represent a Service(bot) account" do favicon: "http://localhost:4001/favicon.png", nodeinfo: %{version: "2.0"} }, - status_ttl_days: nil + status_ttl_days: nil, + permit_followback: false }, pleroma: %{ ap_id: user.ap_id, diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 682c633f4..22f65a0d1 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -258,7 +258,7 @@ test "a note activity" do expected = %{ id: to_string(note.id), uri: object_data["id"], - url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note), + url: url(~p[/notice/#{note}]), account: AccountView.render("show.json", %{user: user, skip_visibility_check: true}), in_reply_to_id: nil, in_reply_to_account_id: nil, diff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs index 731447094..cd3f5eced 100644 --- a/test/pleroma/web/metadata/providers/twitter_card_test.exs +++ b/test/pleroma/web/metadata/providers/twitter_card_test.exs @@ -12,7 +12,6 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do alias Pleroma.Web.MediaProxy alias Pleroma.Web.Metadata.Providers.TwitterCard alias Pleroma.Web.Metadata.Utils - alias Pleroma.Web.Router setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) @@ -189,7 +188,7 @@ test "it renders supported types of attachments and skips unknown types" do {:meta, [ name: "twitter:player", - content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id) + content: url(Endpoint, ~p[/notice/#{activity.id}/embed_player]) ], []}, {:meta, [name: "twitter:player:width", content: "800"], []}, {:meta, [name: "twitter:player:height", content: "600"], []}, diff --git a/test/pleroma/web/mongoose_im_controller_test.exs b/test/pleroma/web/mongoose_im_controller_test.exs index bda7c613f..cf7db6d93 100644 --- a/test/pleroma/web/mongoose_im_controller_test.exs +++ b/test/pleroma/web/mongoose_im_controller_test.exs @@ -13,28 +13,28 @@ test "/user_exists", %{conn: conn} do res = conn - |> get(mongoose_im_path(conn, :user_exists), user: "lain") + |> get(~p"/user_exists", user: "lain") |> json_response(200) assert res == true res = conn - |> get(mongoose_im_path(conn, :user_exists), user: "alice") + |> get(~p"/user_exists", user: "alice") |> json_response(404) assert res == false res = conn - |> get(mongoose_im_path(conn, :user_exists), user: "bob") + |> get(~p"/user_exists", user: "bob") |> json_response(404) assert res == false res = conn - |> get(mongoose_im_path(conn, :user_exists), user: "konata") + |> get(~p"/user_exists", user: "konata") |> json_response(404) assert res == false @@ -52,28 +52,28 @@ test "/check_password", %{conn: conn} do res = conn - |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool") + |> get(~p"/check_password", user: user.nickname, pass: "cool") |> json_response(200) assert res == true res = conn - |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "uncool") + |> get(~p"/check_password", user: user.nickname, pass: "uncool") |> json_response(403) assert res == false res = conn - |> get(mongoose_im_path(conn, :check_password), user: "konata", pass: "cool") + |> get(~p"/check_password", user: "konata", pass: "cool") |> json_response(404) assert res == false res = conn - |> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool") + |> get(~p"/check_password", user: "nobody", pass: "cool") |> json_response(404) assert res == false diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs index 3748940fd..d0703f44c 100644 --- a/test/pleroma/web/o_auth/o_auth_controller_test.exs +++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -57,7 +57,7 @@ test "GET /oauth/authorize renders auth forms, including OAuth consumer form", % assert response = html_response(conn, 200) assert response =~ "Sign in with Twitter" - assert response =~ o_auth_path(conn, :prepare_request) + assert response =~ ~p"/prepare_request" end test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{ @@ -81,9 +81,7 @@ test "GET /oauth/prepare_request encodes parameters as `state` and redirects", % assert html_response(conn, 302) - redirect_query = URI.parse(redirected_to(conn)).query - assert %{"state" => state_param} = URI.decode_query(redirect_query) - assert {:ok, state_components} = Jason.decode(state_param) + assert {:ok, state_components} = Jason.decode(conn.resp_cookies["akkoma_oauth_state"].value) expected_client_id = app.client_id expected_redirect_uri = app.redirect_uris @@ -97,7 +95,7 @@ test "GET /oauth/prepare_request encodes parameters as `state` and redirects", % end test "with user-bound registration, GET /oauth//callback redirects to `redirect_uri` with `code`", - %{app: app, conn: conn} do + %{app: app, conn: _} do registration = insert(:registration) redirect_uri = OAuthController.default_redirect_uri(app) @@ -109,15 +107,17 @@ test "with user-bound registration, GET /oauth//callback redirects to } conn = - conn + build_conn() + |> put_req_cookie("akkoma_oauth_state", Jason.encode!(state_params)) + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid}) |> get( "/oauth/twitter/callback", %{ "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Jason.encode!(state_params) + "provider" => "twitter" } ) @@ -162,15 +162,42 @@ test "with user-unbound registration, GET /oauth//callback renders reg test "on authentication error, GET /oauth//callback redirects to `redirect_uri`", %{ app: app, - conn: conn + conn: _ } do state_params = %{ "scope" => Enum.join(app.scopes, " "), "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "" + "redirect_uri" => OAuthController.default_redirect_uri(app) } + conn = + build_conn() + |> put_req_cookie("akkoma_oauth_state", Jason.encode!(state_params)) + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => "" + } + ) + + assert html_response(conn, 302) + assert redirected_to(conn) == app.redirect_uris + + assert Phoenix.Flash.get(conn.assigns.flash, :error) == + "Failed to authenticate: (error description)." + end + + test "on authentication error with no prior state, GET /oauth//callback returns 400", + %{ + app: _, + conn: conn + } do conn = conn |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) @@ -180,13 +207,11 @@ test "on authentication error, GET /oauth//callback redirects to `redi "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", "provider" => "twitter", - "state" => Jason.encode!(state_params) + "state" => "" } ) - assert html_response(conn, 302) - assert redirected_to(conn) == app.redirect_uris - assert get_flash(conn, :error) == "Failed to authenticate: (error description)." + assert response(conn, 400) end test "GET /oauth/registration_details renders registration details form", %{ @@ -307,7 +332,9 @@ test "with invalid params, POST /oauth/register?op=register renders registration |> post("/oauth/register", bad_params) assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ - assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." + + assert Phoenix.Flash.get(conn.assigns.flash, :error) == + "Error: #{bad_param} has already been taken." end end @@ -398,7 +425,7 @@ test "with invalid params, POST /oauth/register?op=connect renders registration_ |> post("/oauth/register", params) assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ - assert get_flash(conn, :error) == "Invalid Username/Password" + assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid Username/Password" end end diff --git a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs index 7f02bff8f..8829597eb 100644 --- a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs @@ -64,6 +64,10 @@ test "mascot retrieving" do assert json_response_and_validate_schema(ret_conn, 200) + %{"url" => uploaded_url} = Jason.decode!(ret_conn.resp_body) + + assert uploaded_url != nil and is_binary(uploaded_url) + user = User.get_cached_by_id(user.id) conn = @@ -72,6 +76,6 @@ test "mascot retrieving" do |> get("/api/v1/pleroma/mascot") assert %{"url" => url, "type" => "image"} = json_response_and_validate_schema(conn, 200) - assert url =~ "an_image" + assert url == uploaded_url end end diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs index 935e44171..79d4d6261 100644 --- a/test/pleroma/web/static_fe/static_fe_controller_test.exs +++ b/test/pleroma/web/static_fe/static_fe_controller_test.exs @@ -19,9 +19,26 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do setup %{conn: conn} do conn = put_req_header(conn, "accept", "text/html") - user = insert(:user) - %{conn: conn, user: user} + user_avatar_url = "https://example.org/akko.png" + + user = + insert(:user, + local: true, + name: "Akko", + nickname: "atsuko", + bio: "A believing heart is my magic!", + raw_bio: "A believing heart is my magic!", + avatar: %{ + "url" => [ + %{ + "href" => user_avatar_url + } + ] + } + ) + + %{conn: conn, user: user, user_avatar_url: user_avatar_url} end describe "user profile html" do @@ -291,4 +308,142 @@ test "returns 404 for local public activity with `restrict_unauthenticated/activ |> html_response(404) end end + + defp meta_content(metadata_tag) do + :proplists.get_value("content", metadata_tag) + end + + defp meta_find_og(document, name) do + Floki.find(document, "head>meta[property=\"og:" <> name <> "\"]") + end + + defp meta_find_twitter(document, name) do + Floki.find(document, "head>meta[name=\"twitter:" <> name <> "\"]") + end + + # Detailed metadata tests are already done for each builder individually, so just + # one check per type of content should suffice to ensure we're calling the providers correctly + describe "metadata tags for" do + setup do + clear_config([Pleroma.Web.Metadata, :providers], [ + Pleroma.Web.Metadata.Providers.OpenGraph, + Pleroma.Web.Metadata.Providers.TwitterCard + ]) + end + + test "user profile", %{conn: conn, user: user, user_avatar_url: user_avatar_url} do + conn = get(conn, "/users/#{user.nickname}") + html = html_response(conn, 200) + + {:ok, document} = Floki.parse_document(html) + + [{"meta", og_type, _}] = meta_find_og(document, "type") + [{"meta", og_title, _}] = meta_find_og(document, "title") + [{"meta", og_url, _}] = meta_find_og(document, "url") + [{"meta", og_desc, _}] = meta_find_og(document, "description") + [{"meta", og_img, _}] = meta_find_og(document, "image") + [{"meta", og_imgw, _}] = meta_find_og(document, "image:width") + [{"meta", og_imgh, _}] = meta_find_og(document, "image:height") + + [{"meta", tw_card, _}] = meta_find_twitter(document, "card") + [{"meta", tw_title, _}] = meta_find_twitter(document, "title") + [{"meta", tw_desc, _}] = meta_find_twitter(document, "description") + [{"meta", tw_img, _}] = meta_find_twitter(document, "image") + + assert meta_content(og_type) == "article" + assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user) + assert meta_content(og_url) == user.ap_id + assert meta_content(og_desc) == user.bio + assert meta_content(og_img) == user_avatar_url + assert meta_content(og_imgw) == "150" + assert meta_content(og_imgh) == "150" + + assert meta_content(tw_card) == "summary" + assert meta_content(tw_title) == meta_content(og_title) + assert meta_content(tw_desc) == meta_content(og_desc) + assert meta_content(tw_img) == meta_content(og_img) + end + + test "text-only post", %{conn: conn, user: user, user_avatar_url: user_avatar_url} do + post_text = "How are lessons about magic t h i s boring?!" + {:ok, activity} = CommonAPI.post(user, %{status: post_text}) + + conn = get(conn, "/notice/#{activity.id}") + html = html_response(conn, 200) + + {:ok, document} = Floki.parse_document(html) + + [{"meta", og_type, _}] = meta_find_og(document, "type") + [{"meta", og_title, _}] = meta_find_og(document, "title") + [{"meta", og_url, _}] = meta_find_og(document, "url") + [{"meta", og_desc, _}] = meta_find_og(document, "description") + [{"meta", og_img, _}] = meta_find_og(document, "image") + [{"meta", og_imgw, _}] = meta_find_og(document, "image:width") + [{"meta", og_imgh, _}] = meta_find_og(document, "image:height") + + [{"meta", tw_card, _}] = meta_find_twitter(document, "card") + [{"meta", tw_title, _}] = meta_find_twitter(document, "title") + [{"meta", tw_desc, _}] = meta_find_twitter(document, "description") + [{"meta", tw_img, _}] = meta_find_twitter(document, "image") + + assert meta_content(og_type) == "article" + assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user) + assert meta_content(og_url) == activity.data["id"] + assert meta_content(og_desc) == post_text + assert meta_content(og_img) == user_avatar_url + assert meta_content(og_imgw) == "150" + assert meta_content(og_imgh) == "150" + + assert meta_content(tw_card) == "summary" + assert meta_content(tw_title) == meta_content(og_title) + assert meta_content(tw_desc) == meta_content(og_desc) + assert meta_content(tw_img) == meta_content(og_img) + end + + test "post with attachments", %{conn: conn, user: user} do + file = %Plug.Upload{ + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + alt_text = "The rarest of all Shiny Chariot cards" + {:ok, upload_data} = ActivityPub.upload(file, actor: user.ap_id, description: alt_text) + + %{id: media_id, data: %{"url" => [%{"href" => media_url}]}} = upload_data + + post_text = "Look!" + {:ok, activity} = CommonAPI.post(user, %{status: post_text, media_ids: [media_id]}) + + conn = get(conn, "/notice/#{activity.id}") + html = html_response(conn, 200) + + {:ok, document} = Floki.parse_document(html) + + [{"meta", og_type, _}] = meta_find_og(document, "type") + [{"meta", og_title, _}] = meta_find_og(document, "title") + [{"meta", og_url, _}] = meta_find_og(document, "url") + [{"meta", og_desc, _}] = meta_find_og(document, "description") + [{"meta", og_img, _}] = meta_find_og(document, "image") + [{"meta", og_alt, _}] = meta_find_og(document, "image:alt") + + [{"meta", tw_card, _}] = meta_find_twitter(document, "card") + [{"meta", tw_title, _}] = meta_find_twitter(document, "title") + [{"meta", tw_desc, _}] = meta_find_twitter(document, "description") + [{"meta", tw_player, _}] = meta_find_twitter(document, "player") + + assert meta_content(og_type) == "article" + assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user) + assert meta_content(og_url) == activity.data["id"] + assert meta_content(og_desc) == post_text + assert meta_content(og_img) == media_url + assert meta_content(og_alt) == alt_text + + # Audio and video attachments use "player" and have some more metadata + assert meta_content(tw_card) == "summary_large_image" + assert meta_content(tw_title) == meta_content(og_title) + assert meta_content(tw_desc) == meta_content(og_desc) + assert meta_content(tw_player) == meta_content(og_img) + end + end end diff --git a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs index dc300296c..d2bc7840f 100644 --- a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs +++ b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs @@ -48,9 +48,7 @@ test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} assert conn |> get( - remote_follow_path(conn, :follow, %{ - acct: "https://mastodon.social/users/emelie/statuses/101849165031453009" - }) + ~p[/ostatus_subscribe?#{[acct: "https://mastodon.social/users/emelie/statuses/101849165031453009"]}] ) |> redirected_to() =~ "/notice/" end @@ -77,7 +75,7 @@ test "show follow account page if the `acct` is a account link", %{conn: conn} d response = conn - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get(~p[/ostatus_subscribe?#{[acct: "https://mastodon.social/users/emelie"]}]) |> html_response(200) assert response =~ "Log in to follow" @@ -108,7 +106,7 @@ test "show follow page if the `acct` is a account link", %{conn: conn} do response = conn |> assign(:user, user) - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get(~p[/ostatus_subscribe?#{[acct: "https://mastodon.social/users/emelie"]}]) |> html_response(200) assert response =~ "Remote follow" @@ -129,9 +127,7 @@ test "show follow page with error when user can not be fetched by `acct` link", conn |> assign(:user, user) |> get( - remote_follow_path(conn, :follow, %{ - acct: "https://mastodon.social/users/not_found" - }) + ~p[/ostatus_subscribe?#{[acct: "https://mastodon.social/users/not_found"]}] ) |> html_response(200) @@ -151,7 +147,7 @@ test "required `follow | write:follows` scope", %{conn: conn} do conn |> assign(:user, user) |> assign(:token, read_token) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(~p"/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" @@ -166,7 +162,7 @@ test "follows user", %{conn: conn} do conn |> assign(:user, user) |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(~p"/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) assert redirected_to(conn) == "/users/#{user2.id}" end @@ -178,7 +174,7 @@ test "returns error when user is deactivated", %{conn: conn} do response = conn |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(~p"/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" @@ -194,7 +190,7 @@ test "returns error when user is blocked", %{conn: conn} do response = conn |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(~p"/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" @@ -206,7 +202,7 @@ test "returns error when followee not found", %{conn: conn} do response = conn |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}}) + |> post(~p"/ostatus_subscribe", %{"user" => %{"id" => "jimm"}}) |> response(200) assert response =~ "Error following account" @@ -221,7 +217,7 @@ test "returns success result when user already in followers", %{conn: conn} do conn |> assign(:user, refresh_record(user)) |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(~p"/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) assert redirected_to(conn) == "/users/#{user2.id}" end @@ -243,7 +239,7 @@ test "render the MFA login form", %{conn: conn} do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(~p"/ostatus_subscribe", %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} }) |> response(200) @@ -271,7 +267,7 @@ test "returns error when password is incorrect", %{conn: conn} do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(~p"/ostatus_subscribe", %{ "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id} }) |> response(200) @@ -299,7 +295,7 @@ test "follows", %{conn: conn} do conn = conn |> post( - remote_follow_path(conn, :do_follow), + ~p"/ostatus_subscribe", %{ "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} } @@ -328,7 +324,7 @@ test "returns error when auth code is incorrect", %{conn: conn} do response = conn |> post( - remote_follow_path(conn, :do_follow), + ~p"/ostatus_subscribe", %{ "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} } @@ -347,7 +343,7 @@ test "follows", %{conn: conn} do conn = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(~p"/ostatus_subscribe", %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} }) @@ -360,7 +356,7 @@ test "returns error when followee not found", %{conn: conn} do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(~p"/ostatus_subscribe", %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} }) |> response(200) @@ -373,7 +369,7 @@ test "returns error when login invalid", %{conn: conn} do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(~p"/ostatus_subscribe", %{ "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} }) |> response(200) @@ -387,7 +383,7 @@ test "returns error when password invalid", %{conn: conn} do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(~p"/ostatus_subscribe", %{ "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} }) |> response(200) @@ -403,7 +399,7 @@ test "returns error when user is blocked", %{conn: conn} do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(~p"/ostatus_subscribe", %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} }) |> response(200) diff --git a/test/pleroma/web/uploader_controller_test.exs b/test/pleroma/web/uploader_controller_test.exs index 032895e71..e11a7367f 100644 --- a/test/pleroma/web/uploader_controller_test.exs +++ b/test/pleroma/web/uploader_controller_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.UploaderControllerTest do test "it returns 400 response when process callback isn't alive", %{conn: conn} do res = conn - |> post(uploader_path(conn, :callback, "test-path")) + |> post(~p"/api/v1/pleroma/uploader_callback/test-path") assert res.status == 400 assert res.resp_body == "{\"error\":\"bad request\"}" @@ -34,7 +34,7 @@ test "it returns success result", %{conn: conn} do res = conn - |> post(uploader_path(conn, :callback, "test-path")) + |> post(~p"/api/v1/pleroma/uploader_callback/test-path") |> json_response(200) assert res == %{"upload_path" => "test-path"} diff --git a/test/pleroma/web/web_finger/web_finger_controller_test.exs b/test/pleroma/web/web_finger/web_finger_controller_test.exs index 6b5b7c46c..fe8301fa4 100644 --- a/test/pleroma/web/web_finger/web_finger_controller_test.exs +++ b/test/pleroma/web/web_finger/web_finger_controller_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do import ExUnit.CaptureLog import Pleroma.Factory import Tesla.Mock + import Pleroma.Test.Matchers.XML setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -23,8 +24,10 @@ test "GET host-meta" do assert response.status == 200 - assert response.resp_body == - ~s() + assert_xml_equals( + response.resp_body, + ~s() + ) end test "Webfinger JRD" do diff --git a/test/pleroma/web/xml_test.exs b/test/pleroma/web/xml_test.exs index 49306430b..0dc8d299e 100644 --- a/test/pleroma/web/xml_test.exs +++ b/test/pleroma/web/xml_test.exs @@ -3,6 +3,11 @@ defmodule Pleroma.Web.XMLTest do alias Pleroma.Web.XML + test "parses normal XML" do + data = File.read!("test/fixtures/xml_normal.xml") + assert {:ok, _} = XML.parse_document(data) + end + test "refuses to parse any entities from XML" do data = File.read!("test/fixtures/xml_billion_laughs.xml") assert(:error == XML.parse_document(data)) diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index eab469833..21f031bba 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -27,12 +27,12 @@ defmodule Pleroma.Web.ConnCase do import Plug.Conn import Phoenix.ConnTest use Pleroma.Tests.Helpers - import Pleroma.Web.Router.Helpers alias Pleroma.Config # The default endpoint for testing @endpoint Pleroma.Web.Endpoint + use Pleroma.Web, :verified_routes # Sets up OAuth access with specified scopes defp oauth_access(scopes, opts \\ []) do diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 0ee2aa4a2..5eacc148d 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -23,12 +23,14 @@ defmodule Pleroma.DataCase do using do quote do alias Pleroma.Repo + @endpoint Pleroma.Web.Endpoint import Ecto import Ecto.Changeset import Ecto.Query import Pleroma.DataCase use Pleroma.Tests.Helpers + use Pleroma.Web, :verified_routes # Sets up OAuth access with specified scopes defp oauth_access(scopes, opts \\ []) do diff --git a/test/support/factory.ex b/test/support/factory.ex index 42c940c52..e21b8fc1e 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Factory do alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.UserView @rsa_keys [ "test/fixtures/rsa_keys/key_1.pem", @@ -307,7 +308,7 @@ def add_activity_factory(attrs \\ %{}) do featured_collection_activity(attrs, "Add") end - def remove_activity_factor(attrs \\ %{}) do + def remove_activity_factory(attrs \\ %{}) do featured_collection_activity(attrs, "Remove") end @@ -328,7 +329,7 @@ defp featured_collection_activity(attrs, type) do "target" => user.featured_address, "object" => note.data["object"], "actor" => note.data["actor"], - "type" => "Add", + "type" => type, "to" => [Pleroma.Constants.as_public()], "cc" => [user.follower_address] } @@ -554,6 +555,54 @@ def delete_activity_factory(attrs \\ %{}) do |> Map.merge(attrs) end + def undo_activity_factory(attrs \\ %{}) do + like_activity = attrs[:like_activity] || insert(:like_activity) + attrs = Map.drop(attrs, [:like_activity]) + + data = + %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "type" => "Undo", + "actor" => like_activity.data["actor"], + "to" => like_activity.data["to"], + "object" => like_activity.data["id"], + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "context" => like_activity.data["context"] + } + + %Pleroma.Activity{ + data: data, + actor: data["actor"], + recipients: data["to"] + } + |> Map.merge(attrs) + end + + def update_activity_factory(attrs \\ %{}) do + user = attrs[:user] || insert(:user, nickname: "testuser") + attrs = Map.drop(attrs, [:user]) + + user_data = + UserView.render("user.json", %{user: user}) + |> Map.merge(%{"name" => "new display name"}) + + data = %{ + "type" => "Update", + "to" => [ + user_data["followers"], + "https://www.w3.org/ns/activitystreams#Public" + ], + "actor" => user_data["id"], + "object" => user_data + } + + %Pleroma.Activity{ + actor: user_data["id"], + data: data + } + |> Map.merge(attrs) + end + def oauth_app_factory do %Pleroma.Web.OAuth.App{ client_name: sequence(:client_name, &"Some client #{&1}"), diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 6772a7421..cc0e22af1 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -572,6 +572,7 @@ def get("https://social.stopwatchingus-heidelberg.de/.well-known/host-meta", _, }} end + # Mastodon status via display URL def get( "http://mastodon.example.org/@admin/99541947525187367", _, @@ -581,6 +582,23 @@ def get( {:ok, %Tesla.Env{ status: 200, + url: "http://mastodon.example.org/@admin/99541947525187367", + body: File.read!("test/fixtures/mastodon-note-object.json"), + headers: activitypub_object_headers() + }} + end + + # same status via its canonical ActivityPub id + def get( + "http://mastodon.example.org/users/admin/statuses/99541947525187367", + _, + _, + _ + ) do + {:ok, + %Tesla.Env{ + status: 200, + url: "http://mastodon.example.org/users/admin/statuses/99541947525187367", body: File.read!("test/fixtures/mastodon-note-object.json"), headers: activitypub_object_headers() }} @@ -964,7 +982,7 @@ def get("https://pleroma.local/notice/9kCP7V", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}} end - def get("http://localhost:4001/users/masto_closed/followers", _, _, _) do + def get("http://remote.org/users/masto_closed/followers", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -973,7 +991,7 @@ def get("http://localhost:4001/users/masto_closed/followers", _, _, _) do }} end - def get("http://localhost:4001/users/masto_closed/followers?page=1", _, _, _) do + def get("http://remote.org/users/masto_closed/followers?page=1", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -982,7 +1000,7 @@ def get("http://localhost:4001/users/masto_closed/followers?page=1", _, _, _) do }} end - def get("http://localhost:4001/users/masto_closed/following", _, _, _) do + def get("http://remote.org/users/masto_closed/following", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -991,7 +1009,7 @@ def get("http://localhost:4001/users/masto_closed/following", _, _, _) do }} end - def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do + def get("http://remote.org/users/masto_closed/following?page=1", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -1000,7 +1018,7 @@ def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do }} end - def get("http://localhost:8080/followers/fuser3", _, _, _) do + def get("http://remote.org/followers/fuser3", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -1009,7 +1027,7 @@ def get("http://localhost:8080/followers/fuser3", _, _, _) do }} end - def get("http://localhost:8080/following/fuser3", _, _, _) do + def get("http://remote.org/following/fuser3", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -1018,7 +1036,7 @@ def get("http://localhost:8080/following/fuser3", _, _, _) do }} end - def get("http://localhost:4001/users/fuser2/followers", _, _, _) do + def get("http://remote.org/users/fuser2/followers", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -1027,7 +1045,7 @@ def get("http://localhost:4001/users/fuser2/followers", _, _, _) do }} end - def get("http://localhost:4001/users/fuser2/following", _, _, _) do + def get("http://remote.org/users/fuser2/following", _, _, _) do {:ok, %Tesla.Env{ status: 200, diff --git a/test/support/matchers/list.ex b/test/support/matchers/list.ex new file mode 100644 index 000000000..fa3303e34 --- /dev/null +++ b/test/support/matchers/list.ex @@ -0,0 +1,16 @@ +defmodule Pleroma.Test.Matchers.List do + import ExUnit.Assertions + + def assert_unordered_list_equal(list_a, list_b) when is_list(list_a) and is_list(list_b) do + list_a = Enum.sort(list_a) + list_b = Enum.sort(list_b) + + if list_a != list_b do + flunk("Expected list + #{inspect(list_a)} + to have the same elements as + #{inspect(list_b)} + ") + end + end +end diff --git a/test/support/matchers/uri.ex b/test/support/matchers/uri.ex new file mode 100644 index 000000000..8bd518a40 --- /dev/null +++ b/test/support/matchers/uri.ex @@ -0,0 +1,37 @@ +defmodule Pleroma.Test.Matchers.URI do + import ExUnit.Assertions + + def assert_uri_equals(%URI{} = uri_a, %URI{} = uri_b) do + [:scheme, :authority, :userinfo, :host, :port, :path, :fragment] + |> Enum.each(fn attribute -> + if Map.get(uri_a, attribute) == Map.get(uri_b, attribute) do + :ok + else + flunk("Expected #{uri_a} to match #{uri_b} - #{attribute} does not match") + end + end) + + # And the query string + query_a = URI.decode_query(uri_a.query) + query_b = URI.decode_query(uri_b.query) + + if query_a == query_b do + :ok + else + flunk( + "Expected #{uri_a} to match #{uri_b} - query parameters #{inspect(query_a)} do not match #{inspect(query_b)}" + ) + end + end + + def assert_uri_equals(uri_a, uri_b) when is_binary(uri_a) do + uri_a + |> URI.parse() + |> assert_uri_equals(uri_b) + end + + def assert_uri_equals(%URI{} = uri_a, uri_b) when is_binary(uri_b) do + uri_b = URI.parse(uri_b) + assert_uri_equals(uri_a, uri_b) + end +end diff --git a/test/support/matchers/xml.ex b/test/support/matchers/xml.ex new file mode 100644 index 000000000..9f260ce82 --- /dev/null +++ b/test/support/matchers/xml.ex @@ -0,0 +1,18 @@ +defmodule Pleroma.Test.Matchers.XML do + import ExUnit.Assertions + + def assert_xml_equals(xml_a, xml_b) do + map_a = XmlToMap.naive_map(xml_a) + map_b = XmlToMap.naive_map(xml_b) + + if map_a != map_b do + flunk(~s|Expected XML + #{xml_a} + + to equal + + #{xml_b} + |) + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 0fc7a86b9..22a0f33ee 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -9,6 +9,10 @@ {:ok, _} = Application.ensure_all_started(:ex_machina) +# Prepare and later automatically cleanup upload dir +uploads_dir = Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads], "test/uploads") +File.mkdir_p!(uploads_dir) + ExUnit.after_suite(fn _results -> uploads = Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads], "test/uploads") File.rm_rf!(uploads)