diff --git a/.gitignore b/.gitignore index 8821d8ce1..b7411cb3c 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,4 @@ docs/venv # docker stuff docker-db *.iml +docker-compose.override.yml diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml index 16a4067fe..c8b819ce8 100644 --- a/.woodpecker/test.yml +++ b/.woodpecker/test.yml @@ -7,6 +7,7 @@ matrix: ELIXIR_VERSION: - 1.14 - 1.15 + - 1.16 OTP_VERSION: - 25 - 26 @@ -17,6 +18,8 @@ matrix: OTP_VERSION: 25 - ELIXIR_VERSION: 1.15 OTP_VERSION: 26 + - ELIXIR_VERSION: 1.16 + OTP_VERSION: 26 variables: - &scw-secrets diff --git a/CHANGELOG.md b/CHANGELOG.md index 398b0ee45..c94383649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +## 2024.04 + +## Added +- Support for [FEP-fffd](https://codeberg.org/fediverse/fep/src/branch/main/fep/fffd/fep-fffd.md) (proxy objects) +- Verified support for elixir 1.16 +- Uploadfilter `Pleroma.Upload.Filter.Exiftool.ReadDescription` returns description values to the FE so they can pre fill the image description field + NOTE: this filter MUST be placed before `Exiftool.StripMetadata` to work + +## Changed +- Inbound pipeline error handing was modified somewhat, which should lead to less incomprehensible log spam. Hopefully. +- Uploadfilter `Pleroma.Upload.Filter.Exiftool` was replaced by `Pleroma.Upload.Filter.Exiftool.StripMetadata`; + the latter strips all non-essential metadata by default but can be configured. + To regain the old behaviour of only stripping GPS data set `purge: ["gps:all"]`. +- Uploadfilter `Pleroma.Upload.Filter.Exiftool` has been renamed to `Pleroma.Upload.Filter.Exiftool.StripMetadata` +- MRF.InlineQuotePolicy now prefers to insert display URLs instead of ActivityPub IDs +- Old accounts are no longer listed in WebFinger as aliases; this was breaking spec + +## Fixed +- Issue preventing fetching anything from IPv6-only instances +- Issue allowing post content to leak via opengraph tags despite :estrict\_unauthenticated being set +- Move activities no longer operate on stale user data +- Missing definitions in our JSON-LD context +- Issue mangling newlines in code blocks for RSS/Atom feeds +- static\_fe squeezing non-square avatars and emoji +- Issue leading to properly JSON-LD compacted emoji reactions being rejected +- We now use a standard-compliant Accept header when fetching ActivityPub objects +- /api/pleroma/notification\_settings was rejecting body parameters; + this also broke changing this setting via akkoma-fe +- Issue leading to Mastodon bot accounts being rejected +- Scope misdetection of remote posts resulting from not recognising + JSON-LD-compacted forms of public scope; affected e.g. federation with bovine + +## Removed +- ActivityPub Client-To-Server write API endpoints have been disabled; + read endpoints are planned to be removed next release unless a clear need is demonstrated + ## 2024.03 ## Added diff --git a/Procfile b/Procfile deleted file mode 100644 index 7ac187baa..000000000 --- a/Procfile +++ /dev/null @@ -1,2 +0,0 @@ -web: mix phx.server -release: mix ecto.migrate diff --git a/config/description.exs b/config/description.exs index ec5050be6..d9c90edd7 100644 --- a/config/description.exs +++ b/config/description.exs @@ -222,6 +222,26 @@ } ] }, + %{ + group: :pleroma, + key: Pleroma.Upload.Filter.Exiftool.StripMetadata, + type: :group, + description: "Strip specified metadata from image uploads", + children: [ + %{ + key: :purge, + description: "Metadata fields or groups to strip", + type: {:list, :string}, + suggestions: ["all", "CommonIFD0"] + }, + %{ + key: :preserve, + description: "Metadata fields or groups to preserve (takes precedence over stripping)", + type: {:list, :string}, + suggestions: ["ColorSpaces", "Orientation"] + } + ] + }, %{ group: :pleroma, key: Pleroma.Emails.Mailer, diff --git a/config/test.exs b/config/test.exs index 97d005eab..75751e115 100644 --- a/config/test.exs +++ b/config/test.exs @@ -26,6 +26,8 @@ filters: [], link_name: false +config :pleroma, :media_proxy, base_url: "http://localhost:4001" + config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads" config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true diff --git a/coveralls.json b/coveralls.json deleted file mode 100644 index 8652591ef..000000000 --- a/coveralls.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "skip_files": [ - "test/support", - "lib/mix/tasks/pleroma/benchmark.ex", - "lib/credo/check/consistency/file_location.ex" - ] -} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 58abf189d..6c0e2a7e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,7 +46,7 @@ services: volumes: - .:/opt/akkoma - # Uncomment the following if you want to use a reverse proxy + # Copy this into docker-compose.override.yml and uncomment there if you want to use a reverse proxy #proxy: # image: caddy:2-alpine # restart: unless-stopped diff --git a/docs/docs/administration/CLI_tasks/instance.md b/docs/docs/administration/CLI_tasks/instance.md index 5c93cdb30..3d30d1119 100644 --- a/docs/docs/administration/CLI_tasks/instance.md +++ b/docs/docs/administration/CLI_tasks/instance.md @@ -37,7 +37,8 @@ If any of the options are left unspecified, you will be prompted interactively. - `--static-dir ` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.) - `--listen-ip ` - the ip the app should listen to, defaults to 127.0.0.1 - `--listen-port ` - the port the app should listen to, defaults to 4000 -- `--strip-uploads ` - use ExifTool to strip uploads of sensitive location data +- `--strip-uploads-metadata ` - use ExifTool to strip uploads of metadata when possible +- `--read-uploads-description ` - use ExifTool to read image descriptions from uploads - `--anonymize-uploads ` - randomize uploaded filenames - `--dedupe-uploads ` - store files based on their hash to reduce data storage requirements if duplicates are uploaded with different filenames - `--skip-release-env` - skip generation the release environment file diff --git a/docs/docs/configuration/cheatsheet.md b/docs/docs/configuration/cheatsheet.md index d238d73aa..3b5dfa41e 100644 --- a/docs/docs/configuration/cheatsheet.md +++ b/docs/docs/configuration/cheatsheet.md @@ -654,9 +654,17 @@ 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 +#### Pleroma.Upload.Filter.Exiftool.StripMetadata -This filter only strips the GPS and location metadata with Exiftool leaving color profiles and attributes intact. +This filter strips metadata with Exiftool leaving color profiles and orientation intact. + +* `purge`: List of Exiftool tag names or tag group names to purge +* `preserve`: List of Exiftool tag names or tag group names to preserve even if they occur in the purge list + + +#### Pleroma.Upload.Filter.Exiftool.ReadDescription + +This filter reads the ImageDescription and iptc:Caption-Abstract fields with Exiftool so clients can prefill the media description field. No specific configuration. diff --git a/docs/docs/installation/alpine_linux_en.md b/docs/docs/installation/alpine_linux_en.md index bdfb96d77..e5f88ac30 100644 --- a/docs/docs/installation/alpine_linux_en.md +++ b/docs/docs/installation/alpine_linux_en.md @@ -145,47 +145,13 @@ If you want to open your newly installed instance to the world, you should run n doas apk add nginx ``` -* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it: - -```shell -doas apk add certbot -``` - -and then set it up: - -```shell -doas mkdir -p /var/lib/letsencrypt/ -doas certbot certonly --email -d --standalone -``` - -If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again). - * Copy the example nginx configuration to the nginx folder ```shell doas cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.conf ``` -* Before starting nginx edit the configuration and change it to your needs. You must change change `server_name` and the paths to the certificates. You can use `nano` (install with `apk add nano` if missing). - -``` -server { - server_name your.domain; - listen 80; - ... -} - -server { - server_name your.domain; - listen 443 ssl http2; - ... - ssl_trusted_certificate /etc/letsencrypt/live/your.domain/chain.pem; - ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem; - ... -} -``` - +* Before starting nginx edit the configuration and change it to your needs. You must change change `server_name`. You can use `nano` (install with `apk add nano` if missing). * Enable and start nginx: ```shell @@ -193,10 +159,37 @@ doas rc-update add nginx doas rc-service nginx start ``` -If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run: +* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it: ```shell -doas certbot certonly --email -d --webroot -w /var/lib/letsencrypt/ +doas apk add certbot certbot-nginx +``` + +and then set it up: + +```shell +doas mkdir -p /var/lib/letsencrypt/ +doas certbot --email -d -d --nginx +``` + +If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`. + +To automatically renew, set up a cron job like so: + +```shell +# Enable the crond service +doas rc-update add crond +doas rc-service crond start + +# Test that renewals work +doas certbot renew --cert-name yourinstance.tld --nginx --dry-run + +# Add the renewal task to cron +echo '#!/bin/sh +certbot renew --cert-name yourinstance.tld --nginx +' | doas tee /etc/periodic/daily/renew-akkoma-cert +doas chmod +x /etc/periodic/daily/renew-akkoma-cert + ``` #### OpenRC service diff --git a/docs/docs/installation/arch_linux_en.md b/docs/docs/installation/arch_linux_en.md index 300a5d80f..b4b048b86 100644 --- a/docs/docs/installation/arch_linux_en.md +++ b/docs/docs/installation/arch_linux_en.md @@ -136,16 +136,17 @@ If you want to open your newly installed instance to the world, you should run n sudo pacman -S nginx ``` -* Create directories for available and enabled sites: +* Copy the example nginx configuration: ```shell -sudo mkdir -p /etc/nginx/sites-{available,enabled} +sudo cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.conf ``` -* Append the following line at the end of the `http` block in `/etc/nginx/nginx.conf`: +* Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths) +* Enable and start nginx: -```Nginx -include sites-enabled/*; +```shell +sudo systemctl enable --now nginx.service ``` * Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it: @@ -158,32 +159,18 @@ and then set it up: ```shell sudo mkdir -p /var/lib/letsencrypt/ -sudo certbot certonly --email -d --standalone +sudo certbot --email -d -d --nginx ``` -If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again). +If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`. ---- - -* Copy the example nginx configuration and activate it: +To make sure renewals work, enable the appropriate systemd timer: ```shell -sudo cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/sites-available/akkoma.nginx -sudo ln -s /etc/nginx/sites-available/akkoma.nginx /etc/nginx/sites-enabled/akkoma.nginx +sudo systemctl enable --now certbot-renew.timer ``` -* Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths) -* Enable and start nginx: - -```shell -sudo systemctl enable --now nginx.service -``` - -If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run: - -```shell -sudo certbot certonly --email -d --webroot -w /var/lib/letsencrypt/ -``` +Certificate renewal should be handled automatically by Certbot from now on. #### Other webserver/proxies diff --git a/docs/docs/installation/debian_based_en.md b/docs/docs/installation/debian_based_en.md index 6a0f332ec..5dddabe7f 100644 --- a/docs/docs/installation/debian_based_en.md +++ b/docs/docs/installation/debian_based_en.md @@ -155,23 +155,6 @@ If you want to open your newly installed instance to the world, you should run n sudo apt install nginx ``` -* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it: - -```shell -sudo apt install certbot -``` - -and then set it up: - -```shell -sudo mkdir -p /var/lib/letsencrypt/ -sudo certbot certonly --email -d --standalone -``` - -If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again). - ---- - * Copy the example nginx configuration and activate it: ```shell @@ -186,12 +169,23 @@ sudo ln -s /etc/nginx/sites-available/akkoma.nginx /etc/nginx/sites-enabled/akko sudo systemctl enable --now nginx.service ``` -If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run: +* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it: ```shell -sudo certbot certonly --email -d --webroot -w /var/lib/letsencrypt/ +sudo apt install certbot python3-certbot-nginx ``` +and then set it up: + +```shell +sudo mkdir -p /var/lib/letsencrypt/ +sudo certbot --email -d -d --nginx +``` + +If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`. + +Certificate renewal should be handled automatically by Certbot from now on. + #### Other webserver/proxies You can find example configurations for them in `/opt/akkoma/installation/`. diff --git a/docs/docs/installation/docker_en.md b/docs/docs/installation/docker_en.md index 52c056173..9551b034a 100644 --- a/docs/docs/installation/docker_en.md +++ b/docs/docs/installation/docker_en.md @@ -125,7 +125,26 @@ cp docker-resources/Caddyfile.example docker-resources/Caddyfile Then edit the TLD in your caddyfile to the domain you're serving on. -Uncomment the `caddy` section in the docker compose file, +Copy the commented out `caddy` section in `docker-compose.yml` into a new file called `docker-compose.override.yml` like so: +```yaml +version: "3.7" + +services: + proxy: + image: caddy:2-alpine + restart: unless-stopped + links: + - akkoma + ports: [ + "443:443", + "80:80" + ] + volumes: + - ./docker-resources/Caddyfile:/etc/caddy/Caddyfile + - ./caddy-data:/data + - ./caddy-config:/config +``` + then run `docker compose up -d` again. #### Running a reverse proxy on the host @@ -155,6 +174,12 @@ git pull docker compose restart akkoma db ``` +### Modifying the Docker services +If you want to modify the services defined in the docker compose file, you can +create a new file called `docker-compose.override.yml`. There you can add any +overrides or additional services without worrying about git conflicts when a +new release comes out. + #### Further reading {! installation/further_reading.include !} diff --git a/docs/docs/installation/fedora_based_en.md b/docs/docs/installation/fedora_based_en.md index 3e09f6996..40cdaf19a 100644 --- a/docs/docs/installation/fedora_based_en.md +++ b/docs/docs/installation/fedora_based_en.md @@ -135,23 +135,6 @@ If you want to open your newly installed instance to the world, you should run n sudo dnf install nginx ``` -* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it: - -```shell -sudo dnf install certbot -``` - -and then set it up: - -```shell -sudo mkdir -p /var/lib/letsencrypt/ -sudo certbot certonly --email -d --standalone -``` - -If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again). - ---- - * Copy the example nginx configuration and activate it: ```shell @@ -165,12 +148,23 @@ sudo cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.con sudo systemctl enable --now nginx.service ``` -If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run: +* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it: ```shell -sudo certbot certonly --email -d --webroot -w /var/lib/letsencrypt/ +sudo dnf install certbot python3-certbot-nginx ``` +and then set it up: + +```shell +sudo certbot --email -d -d --nginx +``` + +If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`. + +Certificate renewal should be handled automatically by Certbot from now on. + + #### Other webserver/proxies You can find example configurations for them in `/opt/akkoma/installation/`. diff --git a/docs/docs/installation/generic_dependencies.include b/docs/docs/installation/generic_dependencies.include index b23736d85..d7701a28f 100644 --- a/docs/docs/installation/generic_dependencies.include +++ b/docs/docs/installation/generic_dependencies.include @@ -1,8 +1,8 @@ ## Required dependencies * PostgreSQL 9.6+ -* Elixir 1.14+ -* Erlang OTP 25+ +* Elixir 1.14+ (currently tested up to 1.16) +* Erlang OTP 25+ (currently tested up to OTP26) * git * file / libmagic * gcc (clang might also work) diff --git a/docs/docs/installation/gentoo_en.md b/docs/docs/installation/gentoo_en.md index 76069f645..0e54a3e32 100644 --- a/docs/docs/installation/gentoo_en.md +++ b/docs/docs/installation/gentoo_en.md @@ -201,25 +201,6 @@ Assuming you want to open your newly installed federated social network to, well include sites-enabled/*; ``` -* Setup your SSL cert, using your method of choice or certbot. If using certbot, install it if you haven't already: - -```shell - # emerge --ask app-crypt/certbot app-crypt/certbot-nginx -``` - -and then set it up: - -```shell - # mkdir -p /var/lib/letsencrypt/ - # certbot certonly --email -d --standalone -``` - -If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again). Often the answer to issues with certbot is to use the `--nginx` flag once you have nginx up and running. - -If you are using any additional subdomains, such as for a media proxy, you can re-run the same command with the subdomain in question. When it comes time to renew later, you will not need to run multiple times for each domain, one renew will handle it. - ---- - * Copy the example nginx configuration and activate it: ```shell @@ -237,9 +218,24 @@ Pay special attention to the line that begins with `ssl_ecdh_curve`. It is stong ```shell # rc-update add nginx default - # /etc/init.d/nginx start + # rc-service nginx start ``` +* Setup your SSL cert, using your method of choice or certbot. If using certbot, install it if you haven't already: + +```shell + # emerge --ask app-crypt/certbot app-crypt/certbot-nginx +``` + +and then set it up: + +```shell + # mkdir -p /var/lib/letsencrypt/ + # certbot --email -d -d --nginx +``` + +If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`. + If you are using certbot, it is HIGHLY recommend you set up a cron job that renews your certificate, and that you install the suggested `certbot-nginx` plugin. If you don't do these things, you only have yourself to blame when your instance breaks suddenly because you forgot about it. First, ensure that the command you will be installing into your crontab works. diff --git a/docs/docs/installation/optional/media_graphics_packages.md b/docs/docs/installation/optional/media_graphics_packages.md index ab9f958f9..922635fd0 100644 --- a/docs/docs/installation/optional/media_graphics_packages.md +++ b/docs/docs/installation/optional/media_graphics_packages.md @@ -14,7 +14,7 @@ Note: the packages are not required with the current default settings of Akkoma. `ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images. It is required for the following Akkoma features: - * `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`) + * `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Pleroma.Upload/filters` in `config/config.exs`) * Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`) ## `ffmpeg` @@ -29,4 +29,5 @@ It is required for the following Akkoma features: `exiftool` is media files metadata reader/writer. It is required for the following Akkoma features: - * `Pleroma.Upload.Filters.Exiftool` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`) + * `Pleroma.Upload.Filters.Exiftool.StripMetadata` upload filter (related config: `Pleroma.Upload/filters` in `config/config.exs`) + * `Pleroma.Upload.Filters.Exiftool.ReadDescription` upload filter (related config: `Pleroma.Upload/filters` in `config/config.exs`) diff --git a/docs/docs/installation/otp_en.md b/docs/docs/installation/otp_en.md index 8a8ae077b..cdd1ba95d 100644 --- a/docs/docs/installation/otp_en.md +++ b/docs/docs/installation/otp_en.md @@ -9,7 +9,7 @@ This guide covers a installation using an OTP release. To install Akkoma from so * For installing OTP releases on RedHat-based distros like Fedora and Centos Stream, please follow [this guide](./otp_redhat_en.md) instead. * A (sub)domain pointed to the machine -You will be running commands as root. If you aren't root already, please elevate your priviledges by executing `sudo su`/`su`. +You will be running commands as root. If you aren't root already, please elevate your priviledges by executing `sudo -i`/`su`. While in theory OTP releases are possbile to install on any compatible machine, for the sake of simplicity this guide focuses only on Debian/Ubuntu and Alpine. @@ -176,11 +176,6 @@ su akkoma -s $SHELL -lc "./bin/pleroma stop" ### Setting up nginx and getting Let's Encrypt SSL certificaties -#### Get a Let's Encrypt certificate -```sh -certbot certonly --standalone --preferred-challenges http -d yourinstance.tld -``` - #### Copy Akkoma nginx configuration to the nginx folder The location of nginx configs is dependent on the distro @@ -209,6 +204,14 @@ $EDITOR path-to-nginx-config # Verify that the config is valid nginx -t ``` + +#### Get a Let's Encrypt certificate +```sh +certbot --nginx -d yourinstance.tld -d media.yourinstance.tld +``` + +If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`. + #### Start nginx === "Alpine" @@ -252,32 +255,19 @@ If everything worked, you should see Akkoma-FE when visiting your domain. If tha ## Post installation ### Setting up auto-renew of the Let's Encrypt certificate -```sh -# Create the directory for webroot challenges -mkdir -p /var/lib/letsencrypt - -# Uncomment the webroot method -$EDITOR path-to-nginx-config - -# Verify that the config is valid -nginx -t -``` === "Alpine" ``` - # Restart nginx - rc-service nginx restart - # Start the cron daemon and make it start on boot rc-service crond start rc-update add crond # Ensure the webroot menthod and post hook is working - certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --dry-run --post-hook 'rc-service nginx reload' + certbot renew --cert-name yourinstance.tld --nginx --dry-run # Add it to the daily cron echo '#!/bin/sh - certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --post-hook "rc-service nginx reload" + certbot renew --cert-name yourinstance.tld --nginx ' > /etc/periodic/daily/renew-akkoma-cert chmod +x /etc/periodic/daily/renew-akkoma-cert @@ -286,22 +276,7 @@ nginx -t ``` === "Debian/Ubuntu" - ``` - # Restart nginx - systemctl restart nginx - - # Ensure the webroot menthod and post hook is working - certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --dry-run --post-hook 'systemctl reload nginx' - - # Add it to the daily cron - echo '#!/bin/sh - certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --post-hook "systemctl reload nginx" - ' > /etc/cron.daily/renew-akkoma-cert - chmod +x /etc/cron.daily/renew-akkoma-cert - - # If everything worked the output should contain /etc/cron.daily/renew-akkoma-cert - run-parts --test /etc/cron.daily - ``` + This should be automatically enabled with the `certbot-renew.timer` systemd unit. ## Create your first user and set as admin ```sh diff --git a/docs/docs/installation/otp_redhat_en.md b/docs/docs/installation/otp_redhat_en.md index ea27af6f4..38c1b96db 100644 --- a/docs/docs/installation/otp_redhat_en.md +++ b/docs/docs/installation/otp_redhat_en.md @@ -82,6 +82,7 @@ Other than things bundled in the OTP release Akkoma depends on: * PostgreSQL (also utilizes extensions in postgresql-contrib) * nginx (could be swapped with another reverse proxy but this guide covers only it) * certbot (for Let's Encrypt certificates, could be swapped with another ACME client, but this guide covers only it) + * If you are using certbot, also install the `python3-certbot-nginx` package for the nginx plugin * libmagic/file First, update your system, if not already done: @@ -169,12 +170,6 @@ sudo -Hu akkoma ./bin/pleroma stop ### Setting up nginx and getting Let's Encrypt SSL certificaties -#### Get a Let's Encrypt certificate - -```shell -certbot certonly --standalone --preferred-challenges http -d yourinstance.tld -``` - #### Copy Akkoma nginx configuration to the nginx folder ```shell @@ -195,8 +190,15 @@ sudo nginx -t sudo systemctl start nginx ``` -At this point if you open your (sub)domain in a browser you should see a 502 error, that's because Akkoma is not started yet. +#### Get a Let's Encrypt certificate +```shell +sudo certbot --email -d -d --nginx +``` + +If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`. + +If you're successful with obtaining the certificates, opening your (sub)domain in a browser will result in a 502 error, since Akkoma hasn't been started yet. ### Setting up a system service @@ -239,19 +241,11 @@ sudo nginx -t # Restart nginx sudo systemctl restart nginx -# Ensure the webroot menthod and post hook is working -sudo certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --dry-run --post-hook 'systemctl reload nginx' - -# Add it to the daily cron -echo '#!/bin/sh -certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --post-hook "systemctl reload nginx" -' > /etc/cron.daily/renew-akkoma-cert -sudo chmod +x /etc/cron.daily/renew-akkoma-cert - -# If everything worked the output should contain /etc/cron.daily/renew-akkoma-cert -sudo run-parts --test /etc/cron.daily +# Test that renewals work properly +sudo certbot renew --cert-name yourinstance.tld --nginx --dry-run ``` +Assuming the commands were run successfully, certbot should be able to renew your certificates automatically via the `certbot-renew.timer` systemd unit. ## Create your first user and set as admin ```shell diff --git a/elixir_buildpack.config b/elixir_buildpack.config deleted file mode 100644 index ee9e051a6..000000000 --- a/elixir_buildpack.config +++ /dev/null @@ -1,2 +0,0 @@ -elixir_version=1.14.3 -erlang_version=25.3 diff --git a/installation/nginx/akkoma.nginx b/installation/nginx/akkoma.nginx index 1d91ce22f..bfb1fffb3 100644 --- a/installation/nginx/akkoma.nginx +++ b/installation/nginx/akkoma.nginx @@ -1,12 +1,9 @@ # default nginx site config for Akkoma # -# Simple installation instructions: -# 1. Install your TLS certificate, possibly using Let's Encrypt. -# 2. Replace 'example.tld' with your instance's domain wherever it appears. -# 3. Copy this file to /etc/nginx/sites-available/ and then add a symlink to it -# in /etc/nginx/sites-enabled/ and run 'nginx -s reload' or restart nginx. +# See the documentation at docs.akkoma.dev for your particular distro/OS for +# installation instructions. -proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=10g +proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=1g inactive=720m use_temp_path=off; # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only @@ -15,25 +12,19 @@ upstream phoenix { server 127.0.0.1:4000 max_fails=5 fail_timeout=60s; } -server { - server_name example.tld; - - listen 80; - listen [::]:80; - - # Uncomment this if you need to use the 'webroot' method with certbot. Make sure - # that the directory exists and that it is accessible by the webserver. If you followed - # the guide, you already ran 'mkdir -p /var/lib/letsencrypt' to create the folder. - # You may need to load this file with the ssl server block commented out, run certbot - # to get the certificate, and then uncomment it. - # - # location ~ /\.well-known/acme-challenge { - # root /var/lib/letsencrypt/; - # } - location / { - return 301 https://$server_name$request_uri; - } -} +# If you are setting up TLS certificates without certbot, uncomment the +# following to enable HTTP -> HTTPS redirects. Certbot users don't need to do +# this as it will automatically do this for you. +# server { +# server_name example.tld media.example.tld; +# +# listen 80; +# listen [::]:80; +# +# location / { +# return 301 https://$server_name$request_uri; +# } +# } # Enable SSL session caching for improved performance ssl_session_cache shared:ssl_session_cache:10m; @@ -41,22 +32,29 @@ ssl_session_cache shared:ssl_session_cache:10m; server { server_name example.tld; - listen 443 ssl http2; - listen [::]:443 ssl http2; - ssl_session_timeout 1d; - ssl_session_cache shared:MozSSL:10m; # about 40000 sessions - ssl_session_tickets off; + # Once certbot is set up, this will automatically be updated to listen to + # port 443 with TLS alongside a redirect from plaintext HTTP. + listen 80; + listen [::]:80; - ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem; - ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem; + # If you are not using Certbot, comment out the above and uncomment/edit the following +# listen 443 ssl http2; +# listen [::]:443 ssl http2; +# ssl_session_timeout 1d; +# ssl_session_cache shared:MozSSL:10m; # about 40000 sessions +# ssl_session_tickets off; +# +# ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem; +# ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem; +# +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; +# ssl_prefer_server_ciphers off; +# ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1; +# ssl_stapling on; +# ssl_stapling_verify on; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - ssl_prefer_server_ciphers off; - ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1; - ssl_stapling on; - ssl_stapling_verify on; gzip_vary on; gzip_proxied any; @@ -86,27 +84,22 @@ server { # 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; + # Same as above, will be updated to HTTPS once certbot is set up. + listen 80; + listen [::]:80; - 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 + # If you are not using certbot, comment the above and copy all the ssl + # stuff from above into here. + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml; # the nginx default is 1m, not enough for large media uploads client_max_body_size 16m; diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 44f6b6e70..72f4623ce 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -35,7 +35,8 @@ def run(["gen" | rest]) do static_dir: :string, listen_ip: :string, listen_port: :string, - strip_uploads: :string, + strip_uploads_metadata: :string, + read_uploads_description: :string, anonymize_uploads: :string ], aliases: [ @@ -169,21 +170,38 @@ def run(["gen" | rest]) do ) |> Path.expand() - {strip_uploads_message, strip_uploads_default} = + {strip_uploads_metadata_message, strip_uploads_metadata_default} = if Pleroma.Utils.command_available?("exiftool") do - {"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)", + {"Do you want to strip metadata from uploaded images? This requires exiftool, it was detected as installed. (y/n)", "y"} else - {"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)", + {"Do you want to strip metadata from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)", "n"} end - strip_uploads = + strip_uploads_metadata = get_option( options, - :strip_uploads, - strip_uploads_message, - strip_uploads_default + :strip_uploads_metadata, + strip_uploads_metadata_message, + strip_uploads_metadata_default + ) === "y" + + {read_uploads_description_message, read_uploads_description_default} = + if Pleroma.Utils.command_available?("exiftool") do + {"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as installed. (y/n)", + "y"} + else + {"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)", + "n"} + end + + read_uploads_description = + get_option( + options, + :read_uploads_description, + read_uploads_description_message, + read_uploads_description_default ) === "y" anonymize_uploads = @@ -230,7 +248,8 @@ def run(["gen" | rest]) do listen_port: listen_port, upload_filters: upload_filters(%{ - strip: strip_uploads, + strip_metadata: strip_uploads_metadata, + read_description: read_uploads_description, anonymize: anonymize_uploads }) ) @@ -305,11 +324,20 @@ defp write_robots_txt(static_dir, indexable, template_dir) do end defp upload_filters(filters) when is_map(filters) do + enabled_filters = [] + enabled_filters = - if filters.strip do - [Pleroma.Upload.Filter.Exiftool] + if filters.read_description do + enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.ReadDescription] else - [] + enabled_filters + end + + enabled_filters = + if filters.strip_metadata do + enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.StripMetadata] + else + enabled_filters end enabled_filters = diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 3200175d1..28a86d0aa 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -288,6 +288,7 @@ defp http_children do |> Config.get([]) |> Pleroma.HTTP.AdapterHelper.add_pool_size(pool_size) |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy) + |> Pleroma.HTTP.AdapterHelper.ensure_ipv6() |> Keyword.put(:name, MyFinch) [{Finch, config}] diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex index 8dc269f0f..c3777d8f1 100644 --- a/lib/pleroma/application_requirements.ex +++ b/lib/pleroma/application_requirements.ex @@ -164,7 +164,8 @@ defp do_check_rum!(setting, migrate) do defp check_system_commands!(:ok) do filter_commands_statuses = [ - check_filter(Pleroma.Upload.Filter.Exiftool, "exiftool"), + check_filter(Pleroma.Upload.Filter.Exiftool.StripMetadata, "exiftool"), + check_filter(Pleroma.Upload.Filter.Exiftool.ReadDescription, "exiftool"), check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"), check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"), check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"), diff --git a/lib/pleroma/collections/fetcher.ex b/lib/pleroma/collections/fetcher.ex index a2fcb7794..9ab883cc2 100644 --- a/lib/pleroma/collections/fetcher.ex +++ b/lib/pleroma/collections/fetcher.ex @@ -68,7 +68,10 @@ defp fetch_page_items(id, items \\ []) do items end else - {:error, {"Object has been deleted", _, _}} -> + {:error, :not_found} -> + items + + {:error, :forbidden} -> items {:error, error} -> diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index c299a4947..b62532595 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -22,6 +22,43 @@ defmodule Pleroma.Config.DeprecationWarnings do "\n* `config :pleroma, :instance, :quarantined_instances` is now covered by `:pleroma, :mrf_simple, :reject`"} ] + def check_exiftool_filter do + filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, []) + + if Pleroma.Upload.Filter.Exiftool in filters do + Logger.warning(""" + !!!DEPRECATION WARNING!!! + Your config is using Exiftool as a filter instead of Exiftool.StripMetadata. This should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, Pleroma.Upload, + filters: [Pleroma.Upload.Filter.Exiftool] + ``` + + Is now + + + ``` + config :pleroma, Pleroma.Upload, + filters: [Pleroma.Upload.Filter.Exiftool.StripMetadata] + ``` + """) + + new_config = + filters + |> Enum.map(fn + Pleroma.Upload.Filter.Exiftool -> Pleroma.Upload.Filter.Exiftool.StripMetadata + filter -> filter + end) + + Config.put([Pleroma.Upload, :filters], new_config) + + :error + else + :ok + end + end + def check_simple_policy_tuples do has_strings = Config.get([:mrf_simple]) @@ -184,7 +221,8 @@ def warn do check_simple_policy_tuples(), check_http_adapter(), check_uploader_base_url_set(), - check_uploader_base_url_is_not_base_domain() + check_uploader_base_url_is_not_base_domain(), + check_exiftool_filter() ] |> Enum.reduce(:ok, fn :ok, :ok -> :ok diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index 303ccdf2a..c1d95b3a4 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -65,6 +65,15 @@ def add_pool_size(opts, pool_size) do |> put_in([:pools, :default, :size], pool_size) end + def ensure_ipv6(opts) do + # Default transport opts already enable IPv6, so just ensure they're loaded + opts + |> maybe_add_pools() + |> maybe_add_default_pool() + |> maybe_add_conn_opts() + |> maybe_add_transport_opts() + end + defp maybe_add_pools(opts) do if Keyword.has_key?(opts, :pools) do opts @@ -96,11 +105,15 @@ defp maybe_add_conn_opts(opts) do defp maybe_add_transport_opts(opts) do transport_opts = get_in(opts, [:pools, :default, :conn_opts, :transport_opts]) - unless is_nil(transport_opts) do - opts - else - put_in(opts, [:pools, :default, :conn_opts, :transport_opts], []) - end + opts = + unless is_nil(transport_opts) do + opts + else + put_in(opts, [:pools, :default, :conn_opts, :transport_opts], []) + end + + # IPv6 is disabled and IPv4 enabled by default; ensure we can use both + put_in(opts, [:pools, :default, :conn_opts, :transport_opts, :inet6], true) end @doc """ diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 844251a18..379b361f8 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -178,7 +178,10 @@ def normalize(ap_id, options) when is_binary(ap_id) do ap_id Keyword.get(options, :fetch) -> - Fetcher.fetch_object_from_id!(ap_id, options) + case Fetcher.fetch_object_from_id(ap_id, options) do + {:ok, object} -> object + _ -> nil + end true -> get_cached_by_ap_id(ap_id) diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 6609b8c1a..b9d8dbaaa 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -122,7 +122,7 @@ def refetch_object(%Object{data: %{"id" => id}} = object) do {:ok, object} else {:local, true} -> {:ok, object} - {:id, false} -> {:error, "Object id changed on refetch"} + {:id, false} -> {:error, :id_mismatch} e -> {:error, e} end end @@ -136,10 +136,13 @@ def refetch_object(%Object{data: %{"id" => id}} = object) do def fetch_object_from_id(id, options \\ []) do with %URI{} = uri <- URI.parse(id), # let's check the URI is even vaguely valid first - {:scheme, true} <- {:scheme, uri.scheme == "http" or uri.scheme == "https"}, + {:valid_uri_scheme, true} <- + {:valid_uri_scheme, uri.scheme == "http" or uri.scheme == "https"}, # If we have instance restrictions, apply them here to prevent fetching from unwanted instances - {:ok, nil} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_reject(uri), - {:ok, _} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri), + {:mrf_reject_check, {:ok, nil}} <- + {:mrf_reject_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_reject(uri)}, + {:mrf_accept_check, {:ok, _}} <- + {:mrf_accept_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri)}, {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)}, {_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])}, {_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)}, @@ -151,20 +154,37 @@ def fetch_object_from_id(id, options \\ []) do {:object, data, Object.normalize(activity, fetch: false)} do {:ok, object} else - {:allowed_depth, false} -> - {:error, "Max thread distance exceeded."} + {:allowed_depth, false} = e -> + log_fetch_error(id, e) + {:error, :allowed_depth} - {:scheme, false} -> - {:error, "URI Scheme Invalid"} + {:valid_uri_scheme, _} = e -> + log_fetch_error(id, e) + {:error, :invalid_uri_scheme} - {:transmogrifier, {:error, {:reject, e}}} -> - {:reject, e} + {:mrf_reject_check, _} = e -> + log_fetch_error(id, e) + {:reject, :mrf} - {:transmogrifier, {:reject, e}} -> - {:reject, e} + {:mrf_accept_check, _} = e -> + log_fetch_error(id, e) + {:reject, :mrf} - {:transmogrifier, _} = e -> - {:error, e} + {:containment, reason} = e -> + log_fetch_error(id, e) + {:error, reason} + + {:transmogrifier, {:error, {:reject, reason}}} = e -> + log_fetch_error(id, e) + {:reject, reason} + + {:transmogrifier, {:reject, reason}} = e -> + log_fetch_error(id, e) + {:reject, reason} + + {:transmogrifier, reason} = e -> + log_fetch_error(id, e) + {:error, reason} {:object, data, nil} -> reinject_object(%Object{}, data) @@ -175,17 +195,21 @@ def fetch_object_from_id(id, options \\ []) do {:fetch_object, %Object{} = object} -> {:ok, object} - {:fetch, {:error, error}} -> - {:error, error} - - {:reject, reason} -> - {:reject, reason} + {:fetch, {:error, reason}} = e -> + log_fetch_error(id, e) + {:error, reason} e -> - e + log_fetch_error(id, e) + {:error, e} end end + defp log_fetch_error(id, error) do + Logger.metadata(object: id) + Logger.error("Object rejected while fetching #{id} #{inspect(error)}") + end + defp prepare_activity_params(data) do %{ "type" => "Create", @@ -199,27 +223,6 @@ 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 - else - {:error, %Tesla.Mock.Error{}} -> - nil - - {:error, {"Object has been deleted", _id, _code}} -> - nil - - {:reject, reason} -> - Logger.debug("Rejected #{id} while fetching: #{inspect(reason)}") - nil - - e -> - Logger.error("Error while fetching #{id}: #{inspect(e)}") - nil - end - end - defp make_signature(id, date) do uri = URI.parse(id) @@ -259,8 +262,13 @@ def fetch_and_contain_remote_object_from_id(%{"id" => id}), 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} <- {:local_fetch, Containment.contain_local_fetch(id)}, + with {:valid_uri_scheme, true} <- {:valid_uri_scheme, String.starts_with?(id, "http")}, + %URI{} = uri <- URI.parse(id), + {:mrf_reject_check, {:ok, nil}} <- + {:mrf_reject_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_reject(uri)}, + {:mrf_accept_check, {:ok, _}} <- + {:mrf_accept_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri)}, + {:local_fetch, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)}, {:ok, final_id, body} <- get_object(id), {:ok, data} <- safe_json_decode(body), {_, :ok} <- {:strict_id, Containment.contain_id_to_fetch(final_id, data)}, @@ -271,17 +279,29 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do {:ok, data} else - {:strict_id, _} -> - {:error, "Object's ActivityPub id/url does not match final fetch URL"} + {:strict_id, _} = e -> + log_fetch_error(id, e) + {:error, :id_mismatch} - {:scheme, _} -> - {:error, "Unsupported URI scheme"} + {:mrf_reject_check, _} = e -> + log_fetch_error(id, e) + {:reject, :mrf} - {:local_fetch, _} -> - {:error, "Trying to fetch local resource"} + {:mrf_accept_check, _} = e -> + log_fetch_error(id, e) + {:reject, :mrf} - {:containment, _} -> - {:error, "Object containment failed."} + {:valid_uri_scheme, _} = e -> + log_fetch_error(id, e) + {:error, :invalid_uri_scheme} + + {:local_fetch, _} = e -> + log_fetch_error(id, e) + {:error, :local_resource} + + {:containment, reason} -> + log_fetch_error(id, reason) + {:error, reason} {:error, e} -> {:error, e} @@ -292,7 +312,7 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do end def fetch_and_contain_remote_object_from_id(_id), - do: {:error, "id must be a string"} + do: {:error, :invalid_id} defp check_crossdomain_redirect(final_host, original_url) @@ -324,7 +344,11 @@ def get_object(id) do date = Pleroma.Signature.signed_date() headers = - [{"accept", "application/activity+json"}] + [ + # The first is required by spec, the second provided as a fallback for buggy implementations + {"accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}, + {"accept", "application/activity+json"} + ] |> maybe_date_fetch(date) |> sign_fetch(id, date) @@ -352,8 +376,11 @@ def get_object(id) do {:error, {:content_type, content_type}} end else + {:ok, %{status: code}} when code in [401, 403] -> + {:error, :forbidden} + {:ok, %{status: code}} when code in [404, 410] -> - {:error, {"Object has been deleted", id, code}} + {:error, :not_found} {:error, e} -> {:error, e} diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 1158e9449..ad5c2c655 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -39,8 +39,6 @@ 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()) @@ -63,12 +61,23 @@ defmodule Pleroma.Upload do width: integer(), height: integer(), blurhash: String.t(), + description: String.t(), path: String.t() } @always_enabled_filters [Pleroma.Upload.Filter.Dedupe] - defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path] + defstruct [ + :id, + :name, + :tempfile, + :content_type, + :width, + :height, + :blurhash, + :description, + :path + ] @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()} @doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct." @@ -78,7 +87,7 @@ def store(upload, opts \\ []) do with {:ok, upload} <- prepare_upload(upload, opts), upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), - description = Map.get(opts, :description) || "", + description = Map.get(upload, :description) || "", {_, true} <- {:description_limit, String.length(description) <= Pleroma.Config.get([:instance, :description_limit])}, @@ -154,7 +163,8 @@ defp prepare_upload(%Plug.Upload{} = file, opts) do id: UUID.generate(), name: file.filename, tempfile: file.path, - content_type: file.content_type + content_type: file.content_type, + description: opts.description }} end end @@ -174,7 +184,8 @@ defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do id: UUID.generate(), name: hash <> "." <> ext, tempfile: tmp_path, - content_type: content_type + content_type: content_type, + description: opts.description }} end end @@ -230,13 +241,6 @@ 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]) @@ -244,7 +248,7 @@ def base_url do case uploader do Pleroma.Uploaders.Local -> - choose_base_url(upload_base_url) + upload_base_url Pleroma.Uploaders.S3 -> bucket = Config.get([Pleroma.Uploaders.S3, :bucket]) @@ -270,7 +274,7 @@ def base_url do end _ -> - choose_base_url(public_endpoint, upload_base_url) + public_endpoint || upload_base_url end end end diff --git a/lib/pleroma/upload/filter/exiftool/read_description.ex b/lib/pleroma/upload/filter/exiftool/read_description.ex new file mode 100644 index 000000000..5dfe60cd8 --- /dev/null +++ b/lib/pleroma/upload/filter/exiftool/read_description.ex @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do + @moduledoc """ + Gets a valid description from the related EXIF tags and provides them in the response if no description is provided yet. + It will first check ImageDescription, when that doesn't probide a valid description, it will check iptc:Caption-Abstract. + A valid description means the fields are filled in and not too long (see `:instance, :description_limit`). + """ + @behaviour Pleroma.Upload.Filter + + @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()} + + def filter(%Pleroma.Upload{description: description}) + when is_binary(description), + do: {:ok, :noop} + + def filter(%Pleroma.Upload{tempfile: file} = upload), + do: {:ok, :filtered, upload |> Map.put(:description, read_description_from_exif_data(file))} + + def filter(_, _), do: {:ok, :noop} + + defp read_description_from_exif_data(file) do + nil + |> read_when_empty(file, "-ImageDescription") + |> read_when_empty(file, "-iptc:Caption-Abstract") + end + + defp read_when_empty(current_description, _, _) when is_binary(current_description), + do: current_description + + defp read_when_empty(_, file, tag) do + try do + {tag_content, 0} = + System.cmd("exiftool", ["-b", "-s3", tag, file], + stderr_to_stdout: true, + parallelism: true + ) + + tag_content = String.trim(tag_content) + + if tag_content != "" and + String.length(tag_content) <= + Pleroma.Config.get([:instance, :description_limit]), + do: tag_content, + else: nil + rescue + _ in ErlangError -> nil + end + end +end diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool/strip_metadata.ex similarity index 50% rename from lib/pleroma/upload/filter/exiftool.ex rename to lib/pleroma/upload/filter/exiftool/strip_metadata.ex index 4d3a5f551..912ff6a92 100644 --- a/lib/pleroma/upload/filter/exiftool.ex +++ b/lib/pleroma/upload/filter/exiftool/strip_metadata.ex @@ -2,24 +2,42 @@ # Copyright © 2017-2021 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Upload.Filter.Exiftool do +defmodule Pleroma.Upload.Filter.Exiftool.StripMetadata do @moduledoc """ - Strips GPS related EXIF tags and overwrites the file in place. + Tries to strip all image metadata but colorspace and orientation overwriting the file in place. Also strips or replaces filesystem metadata e.g., timestamps. """ @behaviour Pleroma.Upload.Filter + alias Pleroma.Config + + @purge_default ["all", "CommonIFD0"] + @preserve_default ["ColorSpaceTags", "Orientation"] + @spec filter(Pleroma.Upload.t()) :: {:ok, :noop} | {:ok, :filtered} | {:error, String.t()} # Formats not compatible with exiftool at this time - def filter(%Pleroma.Upload{content_type: "image/heic"}), do: {:ok, :noop} def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop} def filter(%Pleroma.Upload{content_type: "image/svg+xml"}), do: {:ok, :noop} - def filter(%Pleroma.Upload{content_type: "image/jxl"}), do: {:ok, :noop} def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do + purge_args = + Config.get([__MODULE__, :purge], @purge_default) + |> Enum.map(fn mgroup -> "-" <> mgroup <> "=" end) + + preserve_args = + Config.get([__MODULE__, :preserve], @preserve_default) + |> Enum.map(fn mgroup -> "-" <> mgroup end) + |> then(fn + # If -TagsFromFile is not followed by tag selectors, it will copy most available tags + [] -> [] + args -> ["-TagsFromFile", "@" | args] + end) + + args = ["-ignoreMinorErrors", "-overwrite_original" | purge_args] ++ preserve_args ++ [file] + try do - case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do + case System.cmd("exiftool", args, parallelism: true) do {_response, 0} -> {:ok, :filtered} {error, 1} -> {:error, error} end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 8449af620..14414adc4 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -969,15 +969,16 @@ defp maybe_send_registration_email(%User{email: email} = user) when is_binary(em defp maybe_send_registration_email(_), do: {:ok, :noop} - def needs_update?(%User{local: true}), do: false + def needs_update?(user, options \\ []) + def needs_update?(%User{local: true}, _options), do: false + def needs_update?(%User{local: false, last_refreshed_at: nil}, _options), do: true - def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true - - def needs_update?(%User{local: false} = user) do - NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400 + def needs_update?(%User{local: false} = user, options) do + NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= + Keyword.get(options, :maximum_age, 86_400) end - def needs_update?(_), do: true + def needs_update?(_, _options), do: true # "Locked" (self-locked) users demand explicit authorization of follow requests @spec can_direct_follow_local(User.t(), User.t()) :: true | false @@ -1980,10 +1981,10 @@ def html_filter_policy(_), do: Config.get([:markup, :scrub_policy]) def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id) - def get_or_fetch_by_ap_id(ap_id) do + def get_or_fetch_by_ap_id(ap_id, options \\ []) do cached_user = get_cached_by_ap_id(ap_id) - maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id) + maybe_fetched_user = needs_update?(cached_user, options) && fetch_by_ap_id(ap_id) case {cached_user, maybe_fetched_user} do {_, {:ok, %User{} = user}} -> diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1e06bc809..505fa7462 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1705,9 +1705,7 @@ defp collection_private(%{"first" => first}) do Fetcher.fetch_and_contain_remote_object_from_id(first) do {:ok, false} else - {:error, {:ok, %{status: code}}} when code in [401, 403] -> {:ok, true} - {:error, _} = e -> e - e -> {:error, e} + {:error, _} -> {:ok, true} end end @@ -1732,7 +1730,7 @@ def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}") {:error, e} - {:error, {:reject, reason} = e} -> + {:reject, reason} = e -> Logger.debug("Rejected user #{ap_id}: #{inspect(reason)}") {:error, e} diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 793b08f3d..6642f7771 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -12,9 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.ObjectView - alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility @@ -40,11 +38,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do # Note: :following and :followers must be served even without authentication (as via :api) plug( EnsureAuthenticatedPlug - when action in [:read_inbox, :update_outbox, :whoami, :upload_media] + when action in [:read_inbox] ) - plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media]) - plug( Pleroma.Web.Plugs.Cache, [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2] @@ -160,7 +156,9 @@ def maybe_skip_cache(conn, user) do end end - # GET /relay/following + @doc """ + GET /relay/following + """ def relay_following(conn, _params) do with %{halted: false} = conn <- FederatingPlug.call(conn, []) do conn @@ -197,7 +195,9 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d end end - # GET /relay/followers + @doc """ + GET /relay/followers + """ def relay_followers(conn, _params) do with %{halted: false} = conn <- FederatingPlug.call(conn, []) do conn @@ -317,14 +317,6 @@ def internal_fetch(conn, _params) do |> represent_service_actor(conn) end - @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated" - def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do - conn - |> put_resp_content_type("application/activity+json") - |> put_view(UserView) - |> render("user.json", %{user: user}) - end - def read_inbox( %{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{"nickname" => nickname, "page" => page?} = params @@ -375,105 +367,6 @@ def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ |> json(err) end - defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity) - when is_map(object) do - length = - [object["content"], object["summary"], object["name"]] - |> Enum.filter(&is_binary(&1)) - |> Enum.join("") - |> String.length() - - limit = Pleroma.Config.get([:instance, :limit]) - - if length < limit do - object = - object - |> Transmogrifier.strip_internal_fields() - |> Map.put("attributedTo", actor) - |> Map.put("actor", actor) - |> Map.put("id", Utils.generate_object_id()) - - {:ok, Map.put(activity, "object", object)} - else - {:error, - dgettext( - "errors", - "Character limit (%{limit} characters) exceeded, contains %{length} characters", - limit: limit, - length: length - )} - end - end - - defp fix_user_message( - %User{ap_id: actor} = user, - %{"type" => "Delete", "object" => object} = activity - ) do - with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)}, - {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do - {:ok, activity} - else - {:normalize, _} -> - {:error, "No such object found"} - - {:permission, _} -> - {:forbidden, "You can't delete this object"} - end - end - - defp fix_user_message(%User{}, activity) do - {:ok, activity} - end - - def update_outbox( - %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn, - %{"nickname" => nickname} = params - ) do - params = - params - |> Map.drop(["nickname"]) - |> Map.put("id", Utils.generate_activity_id()) - |> Map.put("actor", actor) - - with {:ok, params} <- fix_user_message(user, params), - {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true), - %Activity{data: activity_data} <- Activity.normalize(activity) do - conn - |> put_status(:created) - |> put_resp_header("location", activity_data["id"]) - |> json(activity_data) - else - {:forbidden, message} -> - conn - |> put_status(:forbidden) - |> json(message) - - {:error, message} -> - conn - |> put_status(:bad_request) - |> json(message) - - e -> - Logger.warning(fn -> "AP C2S: #{inspect(e)}" end) - - conn - |> put_status(:bad_request) - |> json("Bad Request") - end - end - - def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do - err = - dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", - nickname: nickname, - as_nickname: user.nickname - ) - - conn - |> put_status(:forbidden) - |> json(err) - end - defp errors(conn, {:error, :not_found}) do conn |> put_status(:not_found) @@ -495,21 +388,6 @@ defp set_requester_reachable(%Plug.Conn{} = conn, _) do conn end - def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do - with {:ok, object} <- - ActivityPub.upload( - file, - actor: User.ap_id(user), - description: Map.get(data, "description") - ) do - Logger.debug(inspect(object)) - - conn - |> put_status(:created) - |> json(object.data) - end - end - def pinned(conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname(nickname) do conn diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex index 20432410b..35682f994 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -6,14 +6,29 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @moduledoc "Force a quote line into the message content." @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Object + defp build_inline_quote(prefix, url) do "

#{prefix}: #{url}
" end - defp has_inline_quote?(content, quote_url) do + defp resolve_urls(quote_url) do + # Fetching here can cause infinite recursion as we run this logic on inbound objects too + # This is probably not a problem - its an exceptional corner case for a local user to quote + # a post which doesn't exist + with %Object{} = obj <- Object.normalize(quote_url, fetch: false) do + id = obj.data["id"] + url = Map.get(obj.data, "url", id) + {id, url, [id, url, quote_url]} + else + _ -> {quote_url, quote_url, [quote_url]} + end + end + + defp has_inline_quote?(content, urls) do cond do # Does the quote URL exist in the content? - content =~ quote_url -> true + Enum.any?(urls, fn url -> content =~ url end) -> true # Does the content already have a .quote-inline span? content =~ "" -> true # No inline quote found @@ -22,18 +37,22 @@ defp has_inline_quote?(content, quote_url) do end defp filter_object(%{"quoteUri" => quote_url} = object) do + {id, preferred_url, all_urls} = resolve_urls(quote_url) + object = Map.put(object, "quoteUri", id) + content = object["content"] || "" - if has_inline_quote?(content, quote_url) do + if has_inline_quote?(content, all_urls) do object else prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) content = if String.ends_with?(content, "

") do - String.trim_trailing(content, "

") <> build_inline_quote(prefix, quote_url) <> "

" + String.trim_trailing(content, "

") <> + build_inline_quote(prefix, preferred_url) <> "

" else - content <> build_inline_quote(prefix, quote_url) + content <> build_inline_quote(prefix, preferred_url) end Map.put(object, "content", content) diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index 4d1e80868..d1cd496db 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -53,6 +53,13 @@ def cast_data(data) do defp fix_url(%{"url" => url} = data) when is_bitstring(url), do: data defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"]) + + defp fix_url(%{"url" => url} = data) when is_list(url) do + data + |> Map.put("url", List.first(url)) + |> fix_url() + end + defp fix_url(data), do: data defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex index 80ec65cd7..bda67feee 100644 --- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do alias Pleroma.Emoji alias Pleroma.Object alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -52,6 +53,7 @@ def changeset(struct, data) do defp fix(data) do data = data + |> Transmogrifier.fix_tag() |> fix_emoji_qualification() |> CommonFixes.fix_actor() |> CommonFixes.fix_activity_addressing() diff --git a/lib/pleroma/web/activity_pub/object_validators/user_validator.ex b/lib/pleroma/web/activity_pub/object_validators/user_validator.ex index 90b5404f3..adb291a55 100644 --- a/lib/pleroma/web/activity_pub/object_validators/user_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/user_validator.ex @@ -16,11 +16,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do alias Pleroma.Object.Containment alias Pleroma.Signature + require Pleroma.Constants + @impl true def validate(object, meta) def validate(%{"type" => type, "id" => _id} = data, meta) - when type in ["Person", "Organization", "Group", "Application"] do + when type in Pleroma.Constants.actor_types() do with :ok <- validate_pubkey(data), :ok <- validate_inbox(data), :ok <- contain_collection_origin(data) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 435343bf0..065df5150 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -25,8 +25,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do import Ecto.Query - require Logger require Pleroma.Constants + require Logger @doc """ Modifies an incoming AP object (mastodon format) to our internal format. @@ -58,21 +58,48 @@ def fix_summary(%{"summary" => _} = object) do def fix_summary(object), do: Map.put(object, "summary", "") - def fix_addressing_list(map, field) do - addrs = map[field] - + defp fix_addressing_list(addrs) do cond do - is_list(addrs) -> - Map.put(map, field, Enum.filter(addrs, &is_binary/1)) - - is_binary(addrs) -> - Map.put(map, field, [addrs]) - - true -> - Map.put(map, field, []) + is_list(addrs) -> Enum.filter(addrs, &is_binary/1) + is_binary(addrs) -> [addrs] + true -> [] end end + # Due to JSON-LD simply "Public" and "as:Public" are equivalent to the full URI + # but to simplify later checks we only want to deal with one reperesentation internally + defp normalise_addressing_public_list(map, all_fields) + + defp normalise_addressing_public_list(%{} = map, [field | fields]) do + full_uri = Pleroma.Constants.as_public() + + map = + if map[field] != nil do + new_fval = + map[field] + |> fix_addressing_list() + |> Enum.map(fn + "Public" -> full_uri + "as:Public" -> full_uri + x -> x + end) + + Map.put(map, field, new_fval) + else + map + end + + normalise_addressing_public_list(map, fields) + end + + defp normalise_addressing_public_list(map, _) do + map + end + + defp normalise_addressing_public(map) do + normalise_addressing_public_list(map, ["to", "cc", "bto", "bcc"]) + end + # if directMessage flag is set to true, leave the addressing alone def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection), do: object @@ -96,6 +123,10 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collect |> Map.put("cc", final_cc) end + def fix_addressing_list_key(map, field) do + Map.put(map, field, fix_addressing_list(map[field])) + end + def fix_addressing(object) do {:ok, %User{follower_address: follower_collection}} = object @@ -103,10 +134,10 @@ def fix_addressing(object) do |> User.get_or_fetch_by_ap_id() object - |> fix_addressing_list("to") - |> fix_addressing_list("cc") - |> fix_addressing_list("bto") - |> fix_addressing_list("bcc") + |> fix_addressing_list_key("to") + |> fix_addressing_list_key("cc") + |> fix_addressing_list_key("bto") + |> fix_addressing_list_key("bcc") |> fix_explicit_addressing(follower_collection) |> CommonFixes.fix_implicit_addressing(follower_collection) end @@ -135,8 +166,7 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) |> Map.put("context", replied_object.data["context"] || object["conversation"]) |> Map.drop(["conversation", "inReplyToAtomUri"]) else - e -> - Logger.warning("Couldn't fetch reply@#{inspect(in_reply_to_id)}, error: #{inspect(e)}") + _ -> object end else @@ -384,11 +414,28 @@ defp get_reported(objects) do end) end - def handle_incoming(data, options \\ []) + def handle_incoming(data, options \\ []) do + data = normalise_addressing_public(data) + + data = + if data["object"] != nil do + object = normalise_addressing_public(data["object"]) + Map.put(data, "object", object) + else + data + end + + handle_incoming_normalised(data, options) + end + + defp handle_incoming_normalised(data, options) # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # with nil ID. - def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do + defp handle_incoming_normalised( + %{"type" => "Flag", "object" => objects, "actor" => actor} = data, + _options + ) do with context <- data["context"] || Utils.generate_context_id(), content <- data["content"] || "", %User{} = actor <- User.get_cached_by_ap_id(actor), @@ -409,20 +456,21 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = end # disallow objects with bogus IDs - def handle_incoming(%{"id" => nil}, _options), do: :error - def handle_incoming(%{"id" => ""}, _options), do: :error + defp handle_incoming_normalised(%{"id" => nil}, _options), do: :error + defp handle_incoming_normalised(%{"id" => ""}, _options), do: :error # length of https:// = 8, should validate better, but good enough for now. - def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8, - do: :error + defp handle_incoming_normalised(%{"id" => id}, _options) + when is_binary(id) and byte_size(id) < 8, + do: :error - @doc "Rewrite misskey likes into EmojiReacts" - def handle_incoming( - %{ - "type" => "Like", - "content" => reaction - } = data, - options - ) do + # Rewrite misskey likes into EmojiReacts + defp handle_incoming_normalised( + %{ + "type" => "Like", + "content" => reaction + } = data, + options + ) do if Pleroma.Emoji.is_unicode_emoji?(reaction) || Pleroma.Emoji.matches_shortcode?(reaction) do data |> Map.put("type", "EmojiReact") @@ -434,11 +482,11 @@ def handle_incoming( end end - def handle_incoming( - %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, - options - ) - when objtype in ~w{Question Answer Audio Video Event Article Note Page} do + defp handle_incoming_normalised( + %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, + options + ) + when objtype in ~w{Question Answer Audio Video Event Article Note Page} do fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) object = @@ -470,8 +518,8 @@ def handle_incoming( end end - def handle_incoming(%{"type" => type} = data, _options) - when type in ~w{Like EmojiReact Announce Add Remove} do + defp handle_incoming_normalised(%{"type" => type} = data, _options) + when type in ~w{Like EmojiReact Announce Add Remove} do with :ok <- ObjectValidator.fetch_actor_and_object(data), {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} @@ -481,11 +529,11 @@ def handle_incoming(%{"type" => type} = data, _options) end end - def handle_incoming( - %{"type" => type} = data, - _options - ) - when type in ~w{Update Block Follow Accept Reject} do + defp handle_incoming_normalised( + %{"type" => type} = data, + _options + ) + when type in ~w{Update Block Follow Accept Reject} do with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do @@ -493,10 +541,10 @@ def handle_incoming( end end - def handle_incoming( - %{"type" => "Delete"} = data, - _options - ) do + defp handle_incoming_normalised( + %{"type" => "Delete"} = data, + _options + ) do with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} @@ -516,15 +564,15 @@ def handle_incoming( end end - def handle_incoming( - %{ - "type" => "Undo", - "object" => %{"type" => "Follow", "object" => followed}, - "actor" => follower, - "id" => id - } = _data, - _options - ) do + defp handle_incoming_normalised( + %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "object" => followed}, + "actor" => follower, + "id" => id + } = _data, + _options + ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do @@ -535,28 +583,28 @@ def handle_incoming( end end - def handle_incoming( - %{ - "type" => "Undo", - "object" => %{"type" => type} - } = data, - _options - ) - when type in ["Like", "EmojiReact", "Announce", "Block"] do + defp handle_incoming_normalised( + %{ + "type" => "Undo", + "object" => %{"type" => type} + } = data, + _options + ) + when type in ["Like", "EmojiReact", "Announce", "Block"] do with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} end end # For Undos that don't have the complete object attached, try to find it in our database. - def handle_incoming( - %{ - "type" => "Undo", - "object" => object - } = activity, - options - ) - when is_binary(object) do + defp handle_incoming_normalised( + %{ + "type" => "Undo", + "object" => object + } = activity, + options + ) + when is_binary(object) do with %Activity{data: data} <- Activity.get_by_ap_id(object) do activity |> Map.put("object", data) @@ -566,17 +614,22 @@ def handle_incoming( end end - def handle_incoming( - %{ - "type" => "Move", - "actor" => origin_actor, - "object" => origin_actor, - "target" => target_actor - }, - _options - ) do + defp handle_incoming_normalised( + %{ + "type" => "Move", + "actor" => origin_actor, + "object" => origin_actor, + "target" => target_actor + }, + _options + ) do with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor), - {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor), + # Use a dramatically shortened maximum age before refresh here because it is reasonable + # for a user to + # 1. Add the alias to their new account and then + # 2. Press the button on their new account + # within a very short period of time and expect it to work + {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor, maximum_age: 5), true <- origin_actor in target_user.also_known_as do ActivityPub.move(origin_user, target_user, false) else @@ -584,7 +637,7 @@ def handle_incoming( end end - def handle_incoming(_, _), do: :error + defp handle_incoming_normalised(_, _), do: :error @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil def get_obj_helper(id, options \\ []) do @@ -828,8 +881,7 @@ def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do relative_object do Map.put(data, "object", external_url) else - {:fetch, e} -> - Logger.error("Couldn't fetch fixed_object@#{object} #{inspect(e)}") + {:fetch, _} -> data _ -> diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index fe70022f1..47b8e37e5 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -26,8 +26,7 @@ def render("endpoints.json", %{user: %User{local: true} = _user}) do "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") + "sharedInbox" => url(~p"/inbox") } end diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex index 456ab14db..0f202201d 100644 --- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex +++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex @@ -150,7 +150,7 @@ def update_notificaton_settings_operation do "removes the contents of a message from the push notification" ) ], - requestBody: nil, + requestBody: request_body("Parameters", update_notification_settings_request()), responses: %{ 200 => Operation.response("Success", "application/json", %Schema{ @@ -432,4 +432,22 @@ defp delete_account_request do } } end + + defp update_notification_settings_request do + %Schema{ + title: "UpdateNotificationSettings", + description: "PUT paramenters (query, form or JSON) for updating notification settings", + type: :object, + properties: %{ + block_from_strangers: %Schema{ + type: :boolean, + description: "blocks notifications from accounts you do not follow" + }, + hide_notification_contents: %Schema{ + type: :boolean, + description: "removes the contents of a message from the push notification" + } + } + } + end end diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex index 52771205e..8b0a42dc8 100644 --- a/lib/pleroma/web/feed/feed_view.ex +++ b/lib/pleroma/web/feed/feed_view.ex @@ -78,9 +78,7 @@ def activity_title(%{"content" => content}, opts \\ %{}) do end def activity_content(%{"content" => content}) do - content - |> String.replace(~r/[\n\r]/, "") - |> escape() + escape(content) end def activity_content(_), do: "" diff --git a/lib/pleroma/web/media_proxy.ex b/lib/pleroma/web/media_proxy.ex index 19411d58e..61b6f2a62 100644 --- a/lib/pleroma/web/media_proxy.ex +++ b/lib/pleroma/web/media_proxy.ex @@ -14,8 +14,6 @@ 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() @@ -146,14 +144,8 @@ def filename(url_or_path) do if path = URI.parse(url_or_path).path, do: Path.basename(path) end - 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 + def base_url do + Config.get!([:media_proxy, :base_url]) end defp proxy_url(path, sig_base64, url_base64, filename) do diff --git a/lib/pleroma/web/metadata/providers/open_graph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex index df0cca74a..27e761bc2 100644 --- a/lib/pleroma/web/metadata/providers/open_graph.ex +++ b/lib/pleroma/web/metadata/providers/open_graph.ex @@ -12,14 +12,38 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do @behaviour Provider @media_types ["image", "audio", "video"] + defp user_avatar_tags(user) do + if Utils.visible?(user) do + [ + {:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))], + []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + else + [] + end + end + @impl Provider def build_tags(%{ object: object, url: url, user: user }) do - attachments = build_attachments(object) - scrubbed_content = Utils.scrub_html_and_truncate(object) + attachments = + if Utils.visible?(object) do + build_attachments(object) + else + [] + end + + scrubbed_content = + if Utils.visible?(object) do + Utils.scrub_html_and_truncate(object) + else + "Content cannot be displayed." + end [ {:meta, @@ -36,12 +60,7 @@ def build_tags(%{ {:meta, [property: "og:type", content: "article"], []} ] ++ if attachments == [] or Metadata.activity_nsfw?(object) do - [ - {:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))], - []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] + user_avatar_tags(user) else attachments end @@ -49,7 +68,9 @@ def build_tags(%{ @impl Provider def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do + if Utils.visible?(user) do + truncated_bio = Utils.scrub_html_and_truncate(user.bio) + [ {:meta, [ @@ -58,12 +79,10 @@ def build_tags(%{user: user}) do ], []}, {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, {:meta, [property: "og:description", content: truncated_bio], []}, - {:meta, [property: "og:type", content: "article"], []}, - {:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))], - []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] + {:meta, [property: "og:type", content: "article"], []} + ] ++ user_avatar_tags(user) + else + [] end end diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex index ab48ea272..c6d8464e7 100644 --- a/lib/pleroma/web/metadata/providers/twitter_card.ex +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -17,8 +17,19 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do @impl Provider def build_tags(%{activity_id: id, object: object, user: user}) do - attachments = build_attachments(id, object) - scrubbed_content = Utils.scrub_html_and_truncate(object) + attachments = + if Utils.visible?(object) do + build_attachments(id, object) + else + [] + end + + scrubbed_content = + if Utils.visible?(object) do + Utils.scrub_html_and_truncate(object) + else + "Content cannot be displayed." + end [ title_tag(user), @@ -36,13 +47,17 @@ def build_tags(%{activity_id: id, object: object, user: user}) do @impl Provider def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do - [ - title_tag(user), - {:meta, [name: "twitter:description", content: truncated_bio], []}, - image_tag(user), - {:meta, [name: "twitter:card", content: "summary"], []} - ] + if Utils.visible?(user) do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do + [ + title_tag(user), + {:meta, [name: "twitter:description", content: truncated_bio], []}, + image_tag(user), + {:meta, [name: "twitter:card", content: "summary"], []} + ] + end + else + [] end end @@ -51,7 +66,11 @@ defp title_tag(user) do end def image_tag(user) do - {:meta, [name: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], []} + if Utils.visible?(user) do + {:meta, [name: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], []} + else + {:meta, [name: "twitter:image", content: ""], []} + end end defp build_attachments(id, %{data: %{"attachment" => attachments}}) do diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 92622ef15..55855139b 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -7,6 +7,15 @@ defmodule Pleroma.Web.Metadata.Utils do alias Pleroma.Emoji alias Pleroma.Formatter alias Pleroma.HTML + alias Pleroma.Web.ActivityPub.Visibility + + def visible?(%Pleroma.User{} = object) do + Visibility.restrict_unauthenticated_access?(object) == :visible + end + + def visible?(object) do + Visibility.visible_for_user?(object, nil) + end defp scrub_html_and_truncate_object_field(field, object) do field diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index ca4995281..49ab3540b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -800,13 +800,9 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web.ActivityPub do pipe_through([:activitypub_client]) - get("/api/ap/whoami", ActivityPubController, :whoami) get("/users/:nickname/inbox", ActivityPubController, :read_inbox) get("/users/:nickname/outbox", ActivityPubController, :outbox) - post("/users/:nickname/outbox", ActivityPubController, :update_outbox) - post("/api/ap/upload_media", ActivityPubController, :upload_media) - get("/users/:nickname/collections/featured", ActivityPubController, :pinned) end diff --git a/lib/pleroma/web/telemetry.ex b/lib/pleroma/web/telemetry.ex index 269f9f238..0ddb2c903 100644 --- a/lib/pleroma/web/telemetry.ex +++ b/lib/pleroma/web/telemetry.ex @@ -151,41 +151,40 @@ defp summary_fallback_metrics(byte_unit \\ :byte) do # 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 - ] - ) - ] + 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", diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index f2b571fff..5f1a3518d 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -184,7 +184,13 @@ def emoji(conn, _params) do json(conn, emoji) end - def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do + def update_notificaton_settings( + %{assigns: %{user: user}, body_params: body_params} = conn, + params + ) do + # OpenApiSpex 3.x prevents Plug's usual parameter premerging + params = Map.merge(params, body_params) + with {:ok, _} <- User.update_notification_settings(user, params) do json(conn, %{status: "success"}) end diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex index 9be8ef50e..9d5efbb3e 100644 --- a/lib/pleroma/web/web_finger.ex +++ b/lib/pleroma/web/web_finger.ex @@ -65,7 +65,7 @@ defp gather_links(%User{} = user) do end defp gather_aliases(%User{} = user) do - [user.ap_id | user.also_known_as] + [user.ap_id] end def represent_user(user, "JSON") do diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex index e68341e20..7fe7730ea 100644 --- a/lib/pleroma/web/xml.ex +++ b/lib/pleroma/web/xml.ex @@ -26,7 +26,7 @@ def string_from_xpath(xpath, doc) do def parse_document(text) do try do - doc = SweetXml.parse(text, dtd: :none) + doc = SweetXml.parse(text, dtd: :none, quiet: true) {:ok, doc} rescue diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index 9c3d8d1c2..a663a63fe 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -14,7 +14,8 @@ def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do else {:error, :origin_containment_failed} -> {:discard, :origin_containment_failed} {:error, {:reject, reason}} -> {:discard, reason} - e -> e + {:error, _} = e -> e + e -> {:error, e} end end end diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex index ad4d785a1..1bc0e5d0c 100644 --- a/lib/pleroma/workers/remote_fetcher_worker.ex +++ b/lib/pleroma/workers/remote_fetcher_worker.ex @@ -5,10 +5,42 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do alias Pleroma.Object.Fetcher - use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher" + use Pleroma.Workers.WorkerHelper, + queue: "remote_fetcher", + unique: [period: 300, states: Oban.Job.states(), keys: [:op, :id]] @impl Oban.Worker def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do - {:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"]) + case Fetcher.fetch_object_from_id(id, depth: args["depth"]) do + {:ok, _object} -> + :ok + + {:error, :forbidden} -> + {:discard, :forbidden} + + {:error, :not_found} -> + {:discard, :not_found} + + {:error, :allowed_depth} -> + {:discard, :allowed_depth} + + {:error, :invalid_uri_scheme} -> + {:discard, :invalid_uri_scheme} + + {:error, :local_resource} -> + {:discard, :local_resource} + + {:reject, _} -> + {:discard, :reject} + + {:error, :id_mismatch} -> + {:discard, :id_mismatch} + + {:error, _} = e -> + e + + e -> + {:error, e} + end end end diff --git a/lib/pleroma/workers/worker_helper.ex b/lib/pleroma/workers/worker_helper.ex index 4c0a55774..6d27151de 100644 --- a/lib/pleroma/workers/worker_helper.ex +++ b/lib/pleroma/workers/worker_helper.ex @@ -25,12 +25,16 @@ def sidekiq_backoff(attempt, pow \\ 4, base_backoff \\ 15) do defmacro __using__(opts) do caller_module = __CALLER__.module queue = Keyword.fetch!(opts, :queue) + # by default just stop unintended duplicates - this can and should be overridden + # if you want to have a more complex uniqueness constraint + uniqueness = Keyword.get(opts, :unique, period: 1) quote do # Note: `max_attempts` is intended to be overridden in `new/2` call use Oban.Worker, queue: unquote(queue), - max_attempts: 1 + max_attempts: 1, + unique: unquote(uniqueness) alias Oban.Job diff --git a/mix.exs b/mix.exs index b942c6c23..3581ac40e 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("3.12.2"), + version: version("3.13.0"), elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), @@ -125,7 +125,7 @@ defp deps do {:ecto_enum, "~> 1.4"}, {:ecto_sql, "~> 3.10.0"}, {:postgrex, "~> 0.17.2"}, - {:oban, "~> 2.15.2"}, + {:oban, "~> 2.17.8"}, {:gettext, "~> 0.22.3"}, {:bcrypt_elixir, "~> 3.0.1"}, {:fast_sanitize, "~> 0.2.3"}, diff --git a/mix.lock b/mix.lock index 810356345..118f17699 100644 --- a/mix.lock +++ b/mix.lock @@ -3,55 +3,55 @@ "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.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"}, + "benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [: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", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]}, - "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, + "castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "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": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_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.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"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "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.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_psql_extras": {:hex, :ecto_psql_extras, "0.7.15", "0fc29dbae0e444a29bd6abeee4cf3c4c037e692a272478a234a1cc765077dbb1", [: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 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "b6127f3a5c6fc3d84895e4768cc7c199f22b48b67d6c99b13fbf4a374e73f039"}, "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.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"}, + "elixir_make": {:hex, :elixir_make, "0.8.3", "d38d7ee1578d722d89b4d452a3e36bcfdc644c618f0d063b874661876e708683", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "5c99a18571a756d4af7a4d89ca75c28ac899e6103af6f223982f09ce44942cc9"}, + "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "3.1.0", "4d6260486a8cce59e4bf3575fe2dd2a24766546ceeef9f93fcec6f7c62a2827a", [:mix], [{:erlsom, "~> 1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "8fe5f2e75f90bab07ee2161120c2dc038ebcae8135554f5582990f1c8c21f911"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, - "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_aws": {:hex, :ex_aws, "2.5.3", "9c2d05ba0c057395b12c7b5ca6267d14cdaec1d8e65bdf6481fe1fd245accfb4", [: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", "67115f1d399d7ec4d191812ee565c6106cb4b1bbf19a9d4db06f265fd87da97e"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, "ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"}, - "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_doc": {:hex, :ex_doc, "0.32.0", "896afb57b1e00030f6ec8b2e19d3ca99a197afb23858d49d94aea673dc222f12", [: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", "ed2c3e42c558f49bda3ff37e05713432006e1719a6c4a3320c7e4735787374e7"}, "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_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"}, "fast_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"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "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.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"}, + "floki": {:hex, :floki, "0.36.1", "712b7f2ba19a4d5a47dfe3e74d81876c95bbcbee44fe551f0af3d2a388abb3da", [:mix], [], "hexpm", "21ba57abb8204bcc70c439b423fc0dd9f0286de67dc82773a14b0200ada0995f"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, @@ -60,17 +60,17 @@ "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"}, + "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "joken": {:hex, :joken, "2.6.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"}, + "joken": {:hex, :joken, "2.6.1", "2ca3d8d7f83bf7196296a3d9b2ecda421a404634bfc618159981a960020480a1", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ab26122c400b3d254ce7d86ed066d6afad27e70416df947cdcb01e13a7382e68"}, + "jose": {:hex, :jose, "1.11.9", "c861eb99d9e9f62acd071dc5a49ffbeab9014e44490cd85ea3e49e3d36184777", [:mix, :rebar3], [], "hexpm", "b5ccc3749d2e1638c26bed806259df5bc9e438797fe60dc71e9fa0716133899b"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, "linkify": {:git, "https://akkoma.dev/AkkomaGang/linkify.git", "2567e2c1073fa371fd26fd66dfa5bc77b6919c16", []}, "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.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "b21ab7754024af096f2d14247574f55f0063295b", [ref: "b21ab7754024af096f2d14247574f55f0063295b"]}, @@ -82,54 +82,54 @@ "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"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "oban": {:hex, :oban, "2.17.8", "7fd7c8e82c7819afc1b5b5ed8d6d92bf0ecdd7ba170328fb043301eb06d32521", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a2165bf93843b7bcb68182c82725ddd4cb43c0c3719f114e7aa3b6c99c4b6129"}, + "open_api_spex": {:hex, :open_api_spex, "3.18.3", "fefb84fe323cacfc92afdd0ecb9e89bc0261ae00b7e3167ffc2028ce3944de42", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "c0cfc31570199ce7e7520b494a591027da609af45f6bf9adce51e2469b1609fb"}, "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": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"}, "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_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.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": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [: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", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, "plug_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.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"}, + "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, "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.4", "05dd52a119ee4059fa9daa1ab7ce81bc7a8161a2f12e9d42e9d551ffd2ba901c", [:mix, :rebar3], [], "hexpm", "e9ab01ac7fc8572e41eb59385efeb3fb0ff5bf02103816535bacaedf327d0263"}, + "recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"}, "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.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"}, + "swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, - "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"}, + "table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"}, "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"}, - "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]}, - "tesla": {:hex, :tesla, "1.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"}, + "tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"}, "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.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, - "vex": {:hex, :vex, "0.9.1", "cb65348ebd1c4002861b65bef36e524c29d9a879c90119b2d0e674e323124277", [:mix], [], "hexpm", "a0f9f3959d127ad6a6a617c3f607ecfb1bc6f3c59f9c3614a901a46d1765bafe"}, + "vex": {:hex, :vex, "0.9.2", "fe061acc9e0907d983d46b51bf35d58176f0fe6eb7ba3b33c9336401bf42b6d1", [:mix], [], "hexpm", "76e709a9762e98c6b462dfce92e9b5dfbf712839227f2da8add6dd11549b12cb"}, "web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.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"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, } diff --git a/priv/repo/migrations/20220220135625_upload_filter_exiftool_to_exiftool_strip_location.exs b/priv/repo/migrations/20220220135625_upload_filter_exiftool_to_exiftool_strip_location.exs new file mode 100644 index 000000000..0d68a0787 --- /dev/null +++ b/priv/repo/migrations/20220220135625_upload_filter_exiftool_to_exiftool_strip_location.exs @@ -0,0 +1,37 @@ +defmodule Pleroma.Repo.Migrations.UploadFilterExiftoolToExiftoolStripMetadata do + use Ecto.Migration + + alias Pleroma.ConfigDB + + def up, + do: + ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload}) + |> update_filtername( + Pleroma.Upload.Filter.Exiftool, + Pleroma.Upload.Filter.Exiftool.StripMetadata + ) + + def down, + do: + ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload}) + |> update_filtername( + Pleroma.Upload.Filter.Exiftool.StripMetadata, + Pleroma.Upload.Filter.Exiftool + ) + + defp update_filtername(%{value: value}, from_filtername, to_filtername) do + new_value = + value + |> Keyword.update(:filters, [], fn filters -> + filters + |> Enum.map(fn + ^from_filtername -> to_filtername + filter -> filter + end) + end) + + ConfigDB.update_or_create(%{group: :pleroma, key: Pleroma.Upload, value: new_value}) + end + + defp update_filtername(_, _, _), do: nil +end diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 12a6ec16f..6287669b6 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -19,6 +19,7 @@ "toot": "http://joinmastodon.org/ns#", "misskey": "https://misskey-hub.net/ns#", "fedibird": "http://fedibird.com/ns#", + "sharkey": "https://joinsharkey.org/ns#", "value": "schema:value", "sensitive": "as:sensitive", "litepub": "http://litepub.social/ns#", @@ -45,6 +46,14 @@ "contentMap": { "@id": "as:content", "@container": "@language" + }, + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "backgroundUrl": { + "@id": "sharkey:backgroundUrl", + "@type": "@id" } } ] diff --git a/priv/static/static-fe/static-fe.css b/priv/static/static-fe/static-fe.css index 606c07d7e..81a3bc850 100644 --- a/priv/static/static-fe/static-fe.css +++ b/priv/static/static-fe/static-fe.css @@ -117,6 +117,7 @@ .inner-nav a { .inner-nav img { height: 28px; + width: auto; vertical-align: middle; padding-right: 5px } @@ -440,6 +441,7 @@ .user-info .avatar img { .avatar img { border-radius: 3px; box-shadow: var(--avatarShadow); + object-fit: cover; } .user-summary { @@ -642,13 +644,13 @@ @media (max-width: 800px) { } img:not(.u-photo, .fa-icon) { - width: 32px; + width: auto; height: 32px; padding: 0; vertical-align: middle; } .username img:not(.u-photo) { - width: 16px; + width: auto; height: 16px; } \ No newline at end of file diff --git a/test/fixtures/image_with_caption-abstract.jpg b/test/fixtures/image_with_caption-abstract.jpg new file mode 100644 index 000000000..f982ffa81 Binary files /dev/null and b/test/fixtures/image_with_caption-abstract.jpg differ diff --git a/test/fixtures/image_with_imagedescription_and_caption-abstract.jpg b/test/fixtures/image_with_imagedescription_and_caption-abstract.jpg new file mode 100644 index 000000000..c82a269ef Binary files /dev/null and b/test/fixtures/image_with_imagedescription_and_caption-abstract.jpg differ diff --git a/test/fixtures/image_with_imagedescription_and_caption-abstract_whitespaces.jpg b/test/fixtures/image_with_imagedescription_and_caption-abstract_whitespaces.jpg new file mode 100644 index 000000000..a232fd2a1 Binary files /dev/null and b/test/fixtures/image_with_imagedescription_and_caption-abstract_whitespaces.jpg differ diff --git a/test/fixtures/image_with_no_description.jpg b/test/fixtures/image_with_no_description.jpg new file mode 100644 index 000000000..ec6fc4be8 Binary files /dev/null and b/test/fixtures/image_with_no_description.jpg differ diff --git a/test/fixtures/mastodon/service_actor.json b/test/fixtures/mastodon/service_actor.json new file mode 100644 index 000000000..2abd20b5e --- /dev/null +++ b/test/fixtures/mastodon/service_actor.json @@ -0,0 +1,117 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "toot": "http://joinmastodon.org/ns#", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "featuredTags": { + "@id": "toot:featuredTags", + "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "discoverable": "toot:discoverable", + "Device": "toot:Device", + "Ed25519Signature": "toot:Ed25519Signature", + "Ed25519Key": "toot:Ed25519Key", + "Curve25519Key": "toot:Curve25519Key", + "EncryptedMessage": "toot:EncryptedMessage", + "publicKeyBase64": "toot:publicKeyBase64", + "deviceId": "toot:deviceId", + "claim": { + "@type": "@id", + "@id": "toot:claim" + }, + "fingerprintKey": { + "@type": "@id", + "@id": "toot:fingerprintKey" + }, + "identityKey": { + "@type": "@id", + "@id": "toot:identityKey" + }, + "devices": { + "@type": "@id", + "@id": "toot:devices" + }, + "messageFranking": "toot:messageFranking", + "messageType": "toot:messageType", + "cipherText": "toot:cipherText", + "suspended": "toot:suspended", + "memorial": "toot:memorial", + "indexable": "toot:indexable", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://mastodont.cat/users/fediverse", + "type": "Service", + "following": "https://mastodont.cat/users/fediverse/following", + "followers": "https://mastodont.cat/users/fediverse/followers", + "inbox": "https://mastodont.cat/users/fediverse/inbox", + "outbox": "https://mastodont.cat/users/fediverse/outbox", + "featured": "https://mastodont.cat/users/fediverse/collections/featured", + "featuredTags": "https://mastodont.cat/users/fediverse/collections/tags", + "preferredUsername": "fediverse", + "name": "fediverse's stats", + "summary": "

All fediverse alive servers stats. New refactored code!

Ask server info:

@fediverse server example.server

Ask software info:

@fediverse soft mastodon

", + "url": "https://mastodont.cat/@fediverse", + "manuallyApprovesFollowers": false, + "discoverable": true, + "indexable": false, + "published": "2020-05-13T00:00:00Z", + "memorial": false, + "devices": "https://mastodont.cat/users/fediverse/collections/devices", + "publicKey": { + "id": "https://mastodont.cat/users/fediverse#main-key", + "owner": "https://mastodont.cat/users/fediverse", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu2X8LqAR/6j95UUTG02T\nWG+PmNRWnfOl+zjDts3OctyJK7at5AwA+T0be1faHpf+oLREl/dkWXc8VQY2UJzY\n8QTuXXnIkwHAeA7WADB6kPvQhVpfGPgKD0dpAgBz9WHFquMSXcnuyt7q1CDn5wId\nRoUtkCAcg1rOX+lIAoeic5hT0O0sXLJdtaSCTZmGqkF2Cf+/16q8XhRevMRh73vP\nX2PefCr63Iy/Zh5rnVhPluQMyQ6FGxXgd5dEKJRa2kxrhIsrm0TzMX892Ev45AwI\ndppYQOQ+nLOgMYrpFNYdOmizJsn635l18K1r/tyDDAegPp6Kfa8v+BaZdOmNTFKr\n/wIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "tag": [], + "attachment": [ + { + "type": "PropertyValue", + "name": "code", + "value": "https://codeberg.org/spla/stats" + }, + { + "type": "PropertyValue", + "name": "my user-agent", + "value": ""fediverse's stats (fediverse@mastodont.cat)"" + }, + { + "type": "PropertyValue", + "name": "coded by", + "value": "@spla" + } + ], + "endpoints": { + "sharedInbox": "https://mastodont.cat/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://mastodont.cat/system/accounts/avatars/000/149/323/original/33201dbeb139a24a.png" + }, + "image": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://mastodont.cat/system/accounts/headers/000/149/323/original/75c861d59e5a8860.jpeg" + } +} diff --git a/test/fixtures/peertube/actor-videochannel.json b/test/fixtures/peertube/actor-videochannel.json new file mode 100644 index 000000000..3d0ee5980 --- /dev/null +++ b/test/fixtures/peertube/actor-videochannel.json @@ -0,0 +1,72 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "RsaSignature2017": "https://w3id.org/security#RsaSignature2017" + }, + { + "pt": "https://joinpeertube.org/ns#", + "sc": "http://schema.org/", + "playlists": { + "@id": "pt:playlists", + "@type": "@id" + }, + "support": { + "@type": "sc:Text", + "@id": "pt:support" + }, + "icons": "as:icon" + } + ], + "type": "Group", + "id": "https://spectra.video/video-channels/fediforum_demos", + "following": "https://spectra.video/video-channels/fediforum_demos/following", + "followers": "https://spectra.video/video-channels/fediforum_demos/followers", + "playlists": "https://spectra.video/video-channels/fediforum_demos/playlists", + "inbox": "https://spectra.video/video-channels/fediforum_demos/inbox", + "outbox": "https://spectra.video/video-channels/fediforum_demos/outbox", + "preferredUsername": "fediforum_demos", + "url": "https://spectra.video/video-channels/fediforum_demos", + "name": "FediForum Demos", + "endpoints": { + "sharedInbox": "https://spectra.video/inbox" + }, + "publicKey": { + "id": "https://spectra.video/video-channels/fediforum_demos#main-key", + "owner": "https://spectra.video/video-channels/fediforum_demos", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxFVESAz0Z28zhXVJafzg\nKXVWS6yuZdQ4vOuA+k//ioSpNls53pI9vwQyixNa+QLdnXxm51dy//Py49wZbzAV\n2nC2FEnzcCM/EZvA4gzy7wekcjnGIz3equbdLOj3IAJJTSwCvZpW2f0poAa1CUmQ\nDRV5p3t3bjtUX5B9RnhiuDitN8qCzEeEbD9SHoyMDIACl8wXer8eyi5v98CMTHwh\nJYUJZJmS7/SSlJO2aqThEBaAYCUzVxlcXOecF1N1RWjjtwqi9xXxmlJ+teivYyST\nYfCeLmY/zZPY7OjoBxoVcVa/Yj3Wg6Nt+A5co9NATpsXmud7GWx4CvQ00uH/fa7e\nvQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "published": "2024-03-26T19:34:06.073Z", + "icon": [ + { + "type": "Image", + "mediaType": "image/png", + "height": 48, + "width": 48, + "url": "https://spectra.video/lazy-static/avatars/b13e5038-0169-420e-a6bc-4f5e0666fae6.png" + }, + { + "type": "Image", + "mediaType": "image/png", + "height": 120, + "width": 120, + "url": "https://spectra.video/lazy-static/avatars/559b141a-96ec-4161-8889-1111b71abca0.png" + } + ], + "image": { + "type": "Image", + "mediaType": "image/png", + "height": 317, + "width": 1920, + "url": "https://spectra.video/lazy-static/banners/bbe18e2c-79ef-4640-9193-cdd743c964dd.png" + }, + "summary": "Demos from the the FediForum Unconference. For the sake of simplicity, demos are [broken out into playlists](https://spectra.video/c/fediforum_demos/video-playlists) representing each FediForum Event.", + "support": "Check out our site: https://fediforum.org/\nFollow us on Mastodon: https://mastodon.social/@fediforum", + "attributedTo": [ + { + "type": "Person", + "id": "https://spectra.video/accounts/fediforum" + } + ] +} diff --git a/test/fixtures/tesla_mock/aimu@misskey.io.json b/test/fixtures/tesla_mock/aimu@misskey.io.json new file mode 100644 index 000000000..9ff4cb6d0 --- /dev/null +++ b/test/fixtures/tesla_mock/aimu@misskey.io.json @@ -0,0 +1,64 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey.io/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "type": "Person", + "id": "https://misskey.io/users/83ssedkv53", + "inbox": "https://misskey.io/users/83ssedkv53/inbox", + "outbox": "https://misskey.io/users/83ssedkv53/outbox", + "followers": "https://misskey.io/users/83ssedkv53/followers", + "following": "https://misskey.io/users/83ssedkv53/following", + "sharedInbox": "https://misskey.io/inbox", + "endpoints": { + "sharedInbox": "https://misskey.io/inbox" + }, + "url": "https://misskey.io/@aimu", + "preferredUsername": "aimu", + "name": "あいむ", + "summary": "

わずかな作曲要素 巣穴で独り言
Twitter
https://twitter.com/aimu_53
Soundcloud
https://soundcloud.com/aimu-53

", + "icon": { + "type": "Image", + "url": "https://s3.arkjp.net/misskey/webpublic-3f7e93c0-34f5-443c-acc0-f415cb2342b4.jpg", + "sensitive": false, + "name": null + }, + "image": { + "type": "Image", + "url": "https://s3.arkjp.net/misskey/webpublic-2db63d1d-490b-488b-ab62-c93c285f26b6.png", + "sensitive": false, + "name": null + }, + "tag": [], + "manuallyApprovesFollowers": false, + "discoverable": true, + "publicKey": { + "id": "https://misskey.io/users/83ssedkv53#main-key", + "type": "Key", + "owner": "https://misskey.io/users/83ssedkv53", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1ylhePJ6qGHmwHSBP17b\nIosxGaiFKvgDBgZdm8vzvKeRSqJV9uLHfZL3pO/Zt02EwaZd2GohZAtBZEF8DbMA\n3s93WAesvyGF9mjGrYYKlhp/glwyrrrbf+RdD0DLtyDwRRlrxp3pS2lLmv5Tp1Zl\npH+UKpOnNrpQqjHI5P+lEc9bnflzbRrX+UiyLNsVAP80v4wt7SZfT/telrU6mDru\n998UdfhUo7bDKeDsHG1PfLpyhhtfdoZub4kBpkyacHiwAd+CdCjR54Eu7FDwVK3p\nY3JcrT2q5stgMqN1m4QgSL4XAADIotWwDYttTJejM1n9dr+6VWv5bs0F2Q/6gxOp\nu5DQZLk4Q+64U4LWNox6jCMOq3fYe0g7QalJIHnanYQQo+XjoH6S1Aw64gQ3Ip2Y\nZBmZREAOR7GMFVDPFnVnsbCHnIAv16TdgtLgQBAihkWEUuPqITLi8PMu6kMr3uyq\nYkObEfH0TNTcqaiVpoXv791GZLEUV5ROl0FSUANLNkHZZv29xZ5JDOBOR1rNBLyH\ngVtW8rpszYqOXwzX23hh4WsVXfB7YgNvIijwjiaWbzsecleaENGEnLNMiVKVumTj\nmtyTeFJpH0+OaSrUYpemRRJizmqIjklKsNwUEwUb2WcUUg92o56T2obrBkooabZe\nwgSXSKTOcjsR/ju7+AuIyvkCAwEAAQ==\n-----END PUBLIC KEY-----\n" + }, + "isCat": true, + "vcard:bday": "5353-05-03" +} diff --git a/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json new file mode 100644 index 000000000..323ca10ed --- /dev/null +++ b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json @@ -0,0 +1,44 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey.io/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "id": "https://misskey.io/notes/8vs6wxufd0", + "type": "Note", + "attributedTo": "https://misskey.io/users/83ssedkv53", + "summary": null, + "content": "

Fantiaこれできないように過去のやつは従量課金だった気がする

", + "_misskey_content": "Fantiaこれできないように過去のやつは従量課金だった気がする", + "published": "2022-01-21T16:37:12.663Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://misskey.io/users/83ssedkv53/followers" + ], + "inReplyTo": null, + "attachment": [], + "sensitive": false, + "tag": [] +} diff --git a/test/mix/tasks/pleroma/instance_test.exs b/test/mix/tasks/pleroma/instance_test.exs index 17b2e3267..168343ce5 100644 --- a/test/mix/tasks/pleroma/instance_test.exs +++ b/test/mix/tasks/pleroma/instance_test.exs @@ -69,7 +69,9 @@ test "running gen" do "test/uploads", "--static-dir", "./test/../test/instance/static/", - "--strip-uploads", + "--strip-uploads-metadata", + "y", + "--read-uploads-description", "y", "--anonymize-uploads", "n" @@ -91,7 +93,10 @@ test "running gen" do assert generated_config =~ "password: \"dbpass\"" 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 =~ + "filters: [Pleroma.Upload.Filter.Exiftool.ReadDescription, Pleroma.Upload.Filter.Exiftool.StripMetadata]" + 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")) diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index 96d6fd739..fd8a558ee 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -11,6 +11,62 @@ defmodule Pleroma.Config.DeprecationWarningsTest do alias Pleroma.Config alias Pleroma.Config.DeprecationWarnings + describe "filter exiftool" do + test "gives warning when still used" do + clear_config( + [Pleroma.Upload, :filters], + [Pleroma.Upload.Filter.Exiftool] + ) + + assert capture_log(fn -> DeprecationWarnings.check_exiftool_filter() end) =~ + """ + !!!DEPRECATION WARNING!!! + Your config is using Exiftool as a filter instead of Exiftool.StripMetadata. This should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, Pleroma.Upload, + filters: [Pleroma.Upload.Filter.Exiftool] + ``` + + Is now + + + ``` + config :pleroma, Pleroma.Upload, + filters: [Pleroma.Upload.Filter.Exiftool.StripMetadata] + ``` + """ + end + + test "changes setting to exiftool strip metadata" do + clear_config( + [Pleroma.Upload, :filters], + [Pleroma.Upload.Filter.Exiftool, Pleroma.Upload.Filter.Exiftool.ReadDescription] + ) + + expected_config = [ + Pleroma.Upload.Filter.Exiftool.StripMetadata, + Pleroma.Upload.Filter.Exiftool.ReadDescription + ] + + capture_log(fn -> DeprecationWarnings.warn() end) + + assert Config.get([Pleroma.Upload]) |> Keyword.get(:filters, []) == expected_config + end + + test "doesn't give a warning with correct config" do + clear_config( + [Pleroma.Upload, :filters], + [ + Pleroma.Upload.Filter.Exiftool.StripMetadata, + Pleroma.Upload.Filter.Exiftool.ReadDescription + ] + ) + + assert capture_log(fn -> DeprecationWarnings.check_exiftool_filter() end) == "" + end + end + describe "simple policy tuples" do test "gives warning when there are still strings" do clear_config([:mrf_simple], diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs index 4c4831af3..12154cb05 100644 --- a/test/pleroma/object/fetcher_test.exs +++ b/test/pleroma/object/fetcher_test.exs @@ -57,6 +57,9 @@ defp spoofed_object_with_ids( body: spoofed_object_with_ids("https://patch.cx/objects/spoof_content_type") } + %{method: :get, url: "https://octodon.social/users/cwebber/statuses/111647596861000656"} -> + %Tesla.Env{status: 403} + # Spoof: mismatching ids # Variant 1: Non-exisitng fake id %{ @@ -203,8 +206,7 @@ test "it works when fetching the OP actor errors out" do test "it returns thread depth exceeded error if thread depth is exceeded" do clear_config([:instance, :federation_incoming_replies_max_depth], 0) - assert {:error, "Max thread distance exceeded."} = - Fetcher.fetch_object_from_id(@ap_id, depth: 1) + assert {:error, :allowed_depth} = Fetcher.fetch_object_from_id(@ap_id, depth: 1) end test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do @@ -250,12 +252,12 @@ test "it does not fetch a spoofed object with wrong content type" do 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"} = + assert {:error, :id_mismatch} = 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"} = + assert {:error, :id_mismatch} = Fetcher.fetch_and_contain_remote_object_from_id( "https://patch.cx/media/spoof_stage1.json" ) @@ -285,14 +287,14 @@ test "it accepts same-domain redirects" do end test "it does not fetch a spoofed object with a foreign actor" do - assert {:error, "Object containment failed."} = + assert {:error, _} = 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"} = + assert {:error, :local_resource} = Fetcher.fetch_and_contain_remote_object_from_id( Pleroma.Web.Endpoint.url() <> "/spoof_local" ) @@ -402,16 +404,14 @@ test "all objects with fake directions are rejected by the object fetcher" do end test "handle HTTP 410 Gone response" do - assert {:error, - {"Object has been deleted", "https://mastodon.example.org/users/userisgone", 410}} == + assert {:error, :not_found} == Fetcher.fetch_and_contain_remote_object_from_id( "https://mastodon.example.org/users/userisgone" ) end test "handle HTTP 404 response" do - assert {:error, - {"Object has been deleted", "https://mastodon.example.org/users/userisgone404", 404}} == + assert {:error, :not_found} == Fetcher.fetch_and_contain_remote_object_from_id( "https://mastodon.example.org/users/userisgone404" ) diff --git a/test/pleroma/upload/filter/exiftool/read_description_test.exs b/test/pleroma/upload/filter/exiftool/read_description_test.exs new file mode 100644 index 000000000..7389fda47 --- /dev/null +++ b/test/pleroma/upload/filter/exiftool/read_description_test.exs @@ -0,0 +1,117 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.Exiftool.ReadDescriptionTest do + use Pleroma.DataCase, async: true + alias Pleroma.Upload.Filter + + @uploads %Pleroma.Upload{ + name: "image_with_imagedescription_and_caption-abstract.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"), + tempfile: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"), + description: nil + } + + test "keeps description when not empty" do + uploads = %Pleroma.Upload{ + name: "image_with_imagedescription_and_caption-abstract.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"), + tempfile: + Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"), + description: "Some description" + } + + assert Filter.Exiftool.ReadDescription.filter(uploads) == + {:ok, :noop} + end + + test "otherwise returns ImageDescription when present" do + uploads_after = %Pleroma.Upload{ + name: "image_with_imagedescription_and_caption-abstract.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"), + tempfile: + Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"), + description: "a descriptive white pixel" + } + + assert Filter.Exiftool.ReadDescription.filter(@uploads) == + {:ok, :filtered, uploads_after} + end + + test "otherwise returns iptc:Caption-Abstract when present" do + upload = %Pleroma.Upload{ + name: "image_with_caption-abstract.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image_with_caption-abstract.jpg"), + tempfile: Path.absname("test/fixtures/image_with_caption-abstract.jpg"), + description: nil + } + + upload_after = %Pleroma.Upload{ + name: "image_with_caption-abstract.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image_with_caption-abstract.jpg"), + tempfile: Path.absname("test/fixtures/image_with_caption-abstract.jpg"), + description: "an abstract white pixel" + } + + assert Filter.Exiftool.ReadDescription.filter(upload) == + {:ok, :filtered, upload_after} + end + + test "otherwise returns nil" do + uploads = %Pleroma.Upload{ + name: "image_with_no_description.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image_with_no_description.jpg"), + tempfile: Path.absname("test/fixtures/image_with_no_description.jpg"), + description: nil + } + + assert Filter.Exiftool.ReadDescription.filter(uploads) == + {:ok, :filtered, uploads} + end + + test "Return nil when image description from EXIF data exceeds the maximum length" do + clear_config([:instance, :description_limit], 5) + + assert Filter.Exiftool.ReadDescription.filter(@uploads) == + {:ok, :filtered, @uploads} + end + + test "Ignores content with only whitespace" do + uploads = %Pleroma.Upload{ + name: "non-existant.jpg", + content_type: "image/jpeg", + path: + Path.absname( + "test/fixtures/image_with_imagedescription_and_caption-abstract_whitespaces.jpg" + ), + tempfile: + Path.absname( + "test/fixtures/image_with_imagedescription_and_caption-abstract_whitespaces.jpg" + ), + description: nil + } + + assert Filter.Exiftool.ReadDescription.filter(uploads) == + {:ok, :filtered, uploads} + end + + test "Return nil when image description from EXIF data can't be read" do + uploads = %Pleroma.Upload{ + name: "non-existant.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/non-existant.jpg"), + tempfile: Path.absname("test/fixtures/non-existant_tmp.jpg"), + description: nil + } + + assert Filter.Exiftool.ReadDescription.filter(uploads) == + {:ok, :filtered, uploads} + end +end diff --git a/test/pleroma/upload/filter/exiftool/strip_location_test.exs b/test/pleroma/upload/filter/exiftool/strip_location_test.exs new file mode 100644 index 000000000..2e017cd7e --- /dev/null +++ b/test/pleroma/upload/filter/exiftool/strip_location_test.exs @@ -0,0 +1,148 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.Exiftool.StripMetadataTest do + use Pleroma.DataCase + alias Pleroma.Upload.Filter + + @tag :tmp_dir + test "exiftool strip metadata strips GPS etc but preserves Orientation and ColorSpace by default", + %{tmp_dir: tmp_dir} do + assert Pleroma.Utils.command_available?("exiftool") + + tmpfile = Path.join(tmp_dir, "tmp.jpg") + + File.cp!( + "test/fixtures/DSCN0010.jpg", + tmpfile + ) + + upload = %Pleroma.Upload{ + name: "image_with_GPS_data.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/DSCN0010.jpg"), + tempfile: Path.absname(tmpfile) + } + + assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :filtered} + + exif_original = read_exif("test/fixtures/DSCN0010.jpg") + exif_filtered = read_exif(tmpfile) + + refute exif_original == exif_filtered + assert String.match?(exif_original, ~r/GPS/) + refute String.match?(exif_filtered, ~r/GPS/) + assert String.match?(exif_original, ~r/Camera Model Name/) + refute String.match?(exif_filtered, ~r/Camera Model Name/) + assert String.match?(exif_original, ~r/Orientation/) + assert String.match?(exif_filtered, ~r/Orientation/) + assert String.match?(exif_original, ~r/Color Space/) + assert String.match?(exif_filtered, ~r/Color Space/) + end + + # this is a nonsensical configuration, but it shouldn't explode + @tag :tmp_dir + test "exiftool strip metadata is a noop with empty purge list", %{tmp_dir: tmp_dir} do + assert Pleroma.Utils.command_available?("exiftool") + clear_config([Pleroma.Upload.Filter.Exiftool.StripMetadata, :purge], []) + + tmpfile = Path.join(tmp_dir, "tmp.jpg") + + File.cp!( + "test/fixtures/DSCN0010.jpg", + tmpfile + ) + + upload = %Pleroma.Upload{ + name: "image_with_GPS_data.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/DSCN0010.jpg"), + tempfile: Path.absname(tmpfile) + } + + assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :filtered} + + exif_original = read_exif("test/fixtures/DSCN0010.jpg") + exif_filtered = read_exif(tmpfile) + + assert exif_original == exif_filtered + end + + @tag :tmp_dir + test "exiftool strip metadata works with empty preserve list", %{tmp_dir: tmp_dir} do + assert Pleroma.Utils.command_available?("exiftool") + clear_config([Pleroma.Upload.Filter.Exiftool.StripMetadata, :preserve], []) + + tmpfile = Path.join(tmp_dir, "tmp.jpg") + + File.cp!( + "test/fixtures/DSCN0010.jpg", + tmpfile + ) + + upload = %Pleroma.Upload{ + name: "image_with_GPS_data.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/DSCN0010.jpg"), + tempfile: Path.absname(tmpfile) + } + + write_exif(["-ImageDescription=Trees and Houses", "-Orientation=1", tmpfile]) + exif_extended = read_exif(tmpfile) + assert String.match?(exif_extended, ~r/Image Description[ \t]*:[ \t]*Trees and Houses/) + assert String.match?(exif_extended, ~r/Orientation/) + + assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :filtered} + + exif_original = read_exif("test/fixtures/DSCN0010.jpg") + exif_filtered = read_exif(tmpfile) + + refute exif_original == exif_filtered + refute exif_extended == exif_filtered + assert String.match?(exif_original, ~r/GPS/) + refute String.match?(exif_filtered, ~r/GPS/) + refute String.match?(exif_filtered, ~r/Image Description/) + refute String.match?(exif_filtered, ~r/Orientation/) + end + + test "verify webp files are skipped" do + upload = %Pleroma.Upload{ + name: "sample.webp", + content_type: "image/webp" + } + + assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop} + end + + test "verify svg files are skipped" do + upload = %Pleroma.Upload{ + name: "sample.svg", + content_type: "image/svg+xml" + } + + assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop} + end + + defp read_exif(file) do + # time and file path tags cause mismatches even for byte-identical files + {exif_data, 0} = + System.cmd("exiftool", [ + "-x", + "Time:All", + "-x", + "Directory", + "-x", + "FileName", + "-x", + "FileSize", + file + ]) + + exif_data + end + + defp write_exif(args) do + {_response, 0} = System.cmd("exiftool", ["-ignoreMinorErrors", "-overwrite_original" | args]) + end +end diff --git a/test/pleroma/upload/filter/exiftool_test.exs b/test/pleroma/upload/filter/exiftool_test.exs deleted file mode 100644 index 5f38fa277..000000000 --- a/test/pleroma/upload/filter/exiftool_test.exs +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.ExiftoolTest do - use Pleroma.DataCase - alias Pleroma.Upload.Filter - - test "apply exiftool filter" do - assert Pleroma.Utils.command_available?("exiftool") - - File.cp!( - "test/fixtures/DSCN0010.jpg", - "test/fixtures/DSCN0010_tmp.jpg" - ) - - upload = %Pleroma.Upload{ - name: "image_with_GPS_data.jpg", - content_type: "image/jpeg", - path: Path.absname("test/fixtures/DSCN0010.jpg"), - tempfile: Path.absname("test/fixtures/DSCN0010_tmp.jpg") - } - - assert Filter.Exiftool.filter(upload) == {:ok, :filtered} - - {exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.jpg"]) - {exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.jpg"]) - - refute exif_original == exif_filtered - assert String.match?(exif_original, ~r/GPS/) - refute String.match?(exif_filtered, ~r/GPS/) - end - - test "verify webp files are skipped" do - upload = %Pleroma.Upload{ - name: "sample.webp", - content_type: "image/webp" - } - - assert Filter.Exiftool.filter(upload) == {:ok, :noop} - end -end 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 dcb5f143c..b325bcb9a 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -1418,244 +1418,6 @@ test "It returns poll Answers when authenticated", %{conn: conn} do end end - describe "POST /users/:nickname/outbox (C2S)" do - setup do: clear_config([:instance, :limit]) - - setup do - [ - activity: %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "AP C2S test", - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [] - } - } - ] - end - - test "it rejects posts from other users / unauthenticated users", %{ - conn: conn, - activity: activity - } do - user = insert(:user) - other_user = insert(:user) - conn = put_req_header(conn, "content-type", "application/activity+json") - - conn - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(403) - - conn - |> assign(:user, other_user) - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(403) - end - - test "it inserts an incoming create activity into the database", %{ - conn: conn, - activity: activity - } do - user = insert(:user) - - result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(201) - - assert Activity.get_by_ap_id(result["id"]) - assert result["object"] - assert %Object{data: object} = Object.normalize(result["object"], fetch: false) - assert object["content"] == activity["object"]["content"] - end - - test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do - user = insert(:user) - - activity = - activity - |> put_in(["object", "type"], "Benis") - - _result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(400) - end - - test "it inserts an incoming sensitive activity into the database", %{ - conn: conn, - activity: activity - } do - user = insert(:user) - conn = assign(conn, :user, user) - object = Map.put(activity["object"], "sensitive", true) - activity = Map.put(activity, "object", object) - - response = - conn - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(201) - - assert Activity.get_by_ap_id(response["id"]) - assert response["object"] - assert %Object{data: response_object} = Object.normalize(response["object"], fetch: false) - assert response_object["sensitive"] == true - assert response_object["content"] == activity["object"]["content"] - - representation = - conn - |> put_req_header("accept", "application/activity+json") - |> get(response["id"]) - |> json_response(200) - - assert representation["object"]["sensitive"] == true - end - - test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do - user = insert(:user) - activity = Map.put(activity, "type", "BadType") - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - - assert json_response(conn, 400) - end - - test "it erects a tombstone when receiving a delete activity", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity, fetch: false) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - data = %{ - "type" => "Delete", - "object" => %{ - "id" => note_object.data["id"] - } - } - - result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) - |> json_response(201) - - assert Activity.get_by_ap_id(result["id"]) - - assert object = Object.get_by_ap_id(note_object.data["id"]) - assert object.data["type"] == "Tombstone" - end - - test "it rejects delete activity of object from other actor", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity, fetch: false) - user = insert(:user) - - data = %{ - type: "Delete", - object: %{ - id: note_object.data["id"] - } - } - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) - - assert json_response(conn, 403) - end - - test "it increases like count when receiving a like action", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity, fetch: false) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - data = %{ - type: "Like", - object: %{ - id: note_object.data["id"] - } - } - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) - - result = json_response(conn, 201) - assert Activity.get_by_ap_id(result["id"]) - - assert object = Object.get_by_ap_id(note_object.data["id"]) - assert object.data["like_count"] == 1 - end - - test "it doesn't spreads faulty attributedTo or actor fields", %{ - conn: conn, - activity: activity - } do - reimu = insert(:user, nickname: "reimu") - cirno = insert(:user, nickname: "cirno") - - assert reimu.ap_id - assert cirno.ap_id - - activity = - activity - |> put_in(["object", "actor"], reimu.ap_id) - |> put_in(["object", "attributedTo"], reimu.ap_id) - |> put_in(["actor"], reimu.ap_id) - |> put_in(["attributedTo"], reimu.ap_id) - - _reimu_outbox = - conn - |> assign(:user, cirno) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{reimu.nickname}/outbox", activity) - |> json_response(403) - - cirno_outbox = - conn - |> assign(:user, cirno) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{cirno.nickname}/outbox", activity) - |> json_response(201) - - assert cirno_outbox["attributedTo"] == nil - assert cirno_outbox["actor"] == cirno.ap_id - - assert cirno_object = Object.normalize(cirno_outbox["object"], fetch: false) - assert cirno_object.data["actor"] == cirno.ap_id - assert cirno_object.data["attributedTo"] == cirno.ap_id - end - - test "Character limitation", %{conn: conn, activity: activity} do - clear_config([:instance, :limit], 5) - user = insert(:user) - - result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(400) - - assert result == "Character limit (5 characters) exceeded, contains 11 characters" - end - end - describe "/relay/followers" do test "it returns relay followers", %{conn: conn} do relay_actor = Relay.get_actor() @@ -1977,95 +1739,6 @@ test "it tracks a signed activity fetch when the json is cached", %{conn: conn} end end - describe "Additional ActivityPub C2S endpoints" do - test "GET /api/ap/whoami", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> get("/api/ap/whoami") - - user = User.get_cached_by_id(user.id) - - assert UserView.render("user.json", %{user: user}) == json_response(conn, 200) - - conn - |> get("/api/ap/whoami") - |> json_response(403) - end - - setup do: clear_config([:media_proxy]) - setup do: clear_config([Pleroma.Upload]) - - test "POST /api/ap/upload_media", %{conn: conn} do - user = insert(:user) - - desc = "Description of the image" - - image = %Plug.Upload{ - content_type: "image/jpeg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - object = - conn - |> assign(:user, user) - |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) - |> json_response(:created) - - assert object["name"] == desc - assert object["type"] == "Document" - assert object["actor"] == user.ap_id - assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"] - assert is_binary(object_href) - assert object_mediatype == "image/jpeg" - assert String.ends_with?(object_href, ".jpg") - - activity_request = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "AP C2S test, attachment", - "attachment" => [object], - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [] - } - } - - activity_response = - conn - |> assign(:user, user) - |> post("/users/#{user.nickname}/outbox", activity_request) - |> json_response(:created) - - assert activity_response["id"] - assert activity_response["object"] - assert activity_response["actor"] == user.ap_id - - assert %Object{data: %{"attachment" => [attachment]}} = - Object.normalize(activity_response["object"], fetch: false) - - assert attachment["type"] == "Document" - assert attachment["name"] == desc - - assert [ - %{ - "href" => ^object_href, - "type" => "Link", - "mediaType" => ^object_mediatype - } - ] = attachment["url"] - - # Fails if unauthenticated - conn - |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) - |> json_response(403) - end - end - test "pinned collection", %{conn: conn} do clear_config([:instance, :max_pinned_statuses], 2) user = insert(:user) diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs index 4e0910d3e..7671ad5a1 100644 --- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -4,10 +4,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do alias Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy + alias Pleroma.Object use Pleroma.DataCase + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + test "adds quote URL to post content" do - quote_url = "https://example.com/objects/1234" + quote_url = "https://mastodon.social/users/emelie/statuses/101849165031453009" activity = %{ "type" => "Create", @@ -19,10 +25,13 @@ test "adds quote URL to post content" do } } + # Prefetch the quoted post + %Object{} = Object.normalize(quote_url, fetch: true) + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "

Nice post

RE: https://example.com/objects/1234

" + "

Nice post

RE: https://mastodon.social/@emelie/101849165031453009

" end test "ignores Misskey quote posts" do diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index bcf096a5b..4e96f3200 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -39,6 +39,28 @@ test "a basic note validates", %{note: note} do %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + test "note with url validates", %{note: note} do + note = Map.put(note, "url", "https://remote.example/link") + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) + end + + test "note with url array validates", %{note: note} do + note = Map.put(note, "url", ["https://remote.example/link"]) + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) + end + + test "note with url array validates if contains a link object", %{note: note} do + note = + Map.put(note, "url", [ + %{ + "type" => "Link", + "href" => "https://remote.example/link" + } + ]) + + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) + end + test "a note with a language validates" do insert(:user, %{ap_id: "https://mastodon.social/users/akkoma_ap_integration_tester"}) note = File.read!("test/fixtures/mastodon/note_with_language.json") |> Jason.decode!() 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 f8dec09d3..22a8d0899 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 @@ -12,14 +12,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do 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" - } + 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) diff --git a/test/pleroma/web/activity_pub/object_validators/user_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/user_validator_test.exs new file mode 100644 index 000000000..f96911a81 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/user_validator_test.exs @@ -0,0 +1,38 @@ +# Akkoma: Magically expressive social media +# Copyright © 2024 Akkoma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidatorTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator + + # all standard actor types are listed here: + # https://www.w3.org/TR/activitystreams-vocabulary/#actor-types + describe "accepts standard type" do + test "Application" do + validates_file!("test/fixtures/mastodon/application_actor.json") + end + + test "Group" do + validates_file!("test/fixtures/peertube/actor-videochannel.json") + end + + test "Organization" do + validates_file!("test/fixtures/tesla_mock/wedistribute-user.json") + end + + test "Person" do + validates_file!("test/fixtures/bridgy/actor.json") + end + + test "Service" do + validates_file!("test/fixtures/mastodon/service_actor.json") + end + end + + defp validates_file!(path) do + user_data = Jason.decode!(File.read!(path)) + {:ok, _validated_data, _meta} = UserValidator.validate(user_data, []) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs index 977950434..b40e7f4da 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs @@ -37,7 +37,80 @@ test "it works for incoming emoji reactions" do assert match?([["👌", _, nil]], object.data["reactions"]) end - test "it works for incoming custom emoji reactions" do + test "it works for incoming custom emoji with nil id" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + shortcode = "blobcatgoogly" + emoji = emoji_object(shortcode) + data = react_with_custom(activity.data["object"], other_user.ap_id, emoji) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == other_user.ap_id + assert data["type"] == "EmojiReact" + assert data["object"] == activity.data["object"] + assert data["content"] == ":" <> shortcode <> ":" + [%{}] = data["tag"] + + object = Object.get_by_ap_id(data["object"]) + + assert object.data["reaction_count"] == 1 + assert match?([[^shortcode, _, _]], object.data["reactions"]) + end + + test "it works for incoming custom emoji with image url as id" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + shortcode = "blobcatgoogly" + imgurl = "https://example.org/emoji/a.png" + emoji = emoji_object(shortcode, imgurl, imgurl) + data = react_with_custom(activity.data["object"], other_user.ap_id, emoji) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == other_user.ap_id + assert data["type"] == "EmojiReact" + assert data["object"] == activity.data["object"] + assert data["content"] == ":" <> shortcode <> ":" + assert [%{}] = data["tag"] + + object = Object.get_by_ap_id(data["object"]) + + assert object.data["reaction_count"] == 1 + assert match?([[^shortcode, _, ^imgurl]], object.data["reactions"]) + end + + test "it works for incoming custom emoji without tag array" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + shortcode = "blobcatgoogly" + imgurl = "https://example.org/emoji/b.png" + emoji = emoji_object(shortcode, imgurl, imgurl) + data = react_with_custom(activity.data["object"], other_user.ap_id, emoji, false) + + assert %{} = data["tag"] + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == other_user.ap_id + assert data["type"] == "EmojiReact" + assert data["object"] == activity.data["object"] + assert data["content"] == ":" <> shortcode <> ":" + assert [%{}] = data["tag"] + + object = Object.get_by_ap_id(data["object"]) + + assert object.data["reaction_count"] == 1 + assert match?([[^shortcode, _, _]], object.data["reactions"]) + end + + test "it works for incoming custom emoji reactions from Misskey" do user = insert(:user) other_user = insert(:user, local: false) {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) @@ -138,4 +211,27 @@ test "it reject invalid emoji reactions" do assert {:error, _} = Transmogrifier.handle_incoming(data) end + + defp emoji_object(shortcode, id \\ nil, url \\ "https://example.org/emoji.png") do + %{ + "type" => "Emoji", + "id" => id, + "name" => shortcode |> String.replace_prefix(":", "") |> String.replace_suffix(":", ""), + "icon" => %{ + "type" => "Image", + "url" => url + } + } + end + + defp react_with_custom(object_id, as_actor, emoji, tag_array \\ true) do + tag = if tag_array, do: [emoji], else: emoji + + File.read!("test/fixtures/emoji-reaction.json") + |> Jason.decode!() + |> Map.put("object", object_id) + |> Map.put("actor", as_actor) + |> Map.put("content", ":" <> emoji["name"] <> ":") + |> Map.put("tag", tag) + end end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index e473ae659..dd7977593 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -124,6 +124,28 @@ test "it fixes both the Create and object contexts in a reply" do assert activity.data["context"] == object.data["context"] end + + test "it accepts quote posts" do + insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") + + object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => "https://misskey.io/users/7rkrarq81i", + "object" => object + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + # Object was created in the database + object = Object.normalize(activity) + assert object.data["quoteUri"] == "https://misskey.io/notes/8vs6wxufd0" + + # It fetched the quoted post + assert Object.normalize("https://misskey.io/notes/8vs6wxufd0") + end end describe "prepare outgoing" do @@ -413,7 +435,7 @@ test "it rejects activities which reference objects with bogus origins" do assert capture_log(fn -> {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" + end) =~ "Object rejected while fetching" end test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do @@ -428,7 +450,7 @@ test "it rejects activities which reference objects that have an incorrect attri assert capture_log(fn -> {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" + end) =~ "Object rejected while fetching" end test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do @@ -443,7 +465,7 @@ test "it rejects activities which reference objects that have an incorrect attri assert capture_log(fn -> {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" + end) =~ "Object rejected while fetching" end end @@ -536,7 +558,7 @@ test "returns non-modified object" do test "returns nil when cannot normalize object" do assert capture_log(fn -> refute Transmogrifier.get_obj_helper("test-obj-id") - end) =~ "URI Scheme Invalid" + end) =~ ":valid_uri_scheme" end @tag capture_log: true diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs index 76a7a6d37..d3cc239cf 100644 --- a/test/pleroma/web/federator_test.exs +++ b/test/pleroma/web/federator_test.exs @@ -137,6 +137,37 @@ test "successfully processes incoming AP docs with correct origin" do assert {:error, :already_present} = ObanHelpers.perform(job) end + test "successfully normalises public scope descriptors" do + params = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "http://mastodon.example.org/users/admin", + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/activities/1", + "object" => %{ + "type" => "Note", + "content" => "hi world!", + "id" => "http://mastodon.example.org/users/admin/objects/1", + "attributedTo" => "http://mastodon.example.org/users/admin", + "to" => ["Public"] + }, + "to" => ["as:Public"] + } + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:ok, activity} = ObanHelpers.perform(job) + assert activity.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + object = + from( + object in Pleroma.Object, + where: fragment("(?)->>'id' = ?", object.data, ^activity.data["object"]), + limit: 1 + ) + |> Repo.one() + + assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + end + test "rejects incoming AP docs with incorrect origin" do params = %{ "@context" => "https://www.w3.org/ns/activitystreams", 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 25a18e145..f58045640 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -111,7 +111,7 @@ test "posting a status", %{conn: conn} do # 2 hours expires_in = 2 * 60 * 60 - expires_at = DateTime.add(DateTime.utc_now(), expires_in) + expires_at1 = DateTime.add(DateTime.utc_now(), expires_in) conn_four = conn @@ -123,12 +123,16 @@ test "posting a status", %{conn: conn} do assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200) - assert Activity.get_by_id(fourth_id) + activity = Activity.get_by_id(fourth_id) + assert activity + + {:ok, expires_at2, _} = DateTime.from_iso8601(activity.data["expires_at"]) + assert Timex.compare(expires_at1, expires_at2, :minutes) == 0 assert_enqueued( worker: Pleroma.Workers.PurgeExpiredActivity, args: %{activity_id: fourth_id}, - scheduled_at: expires_at + scheduled_at: expires_at2 ) end @@ -148,16 +152,13 @@ test "automatically setting a post expiry if status_ttl_days is set" do activity = Activity.get_by_id_with_object(id) {:ok, expires_at, _} = DateTime.from_iso8601(activity.data["expires_at"]) - assert Timex.diff( - expires_at, - DateTime.utc_now(), - :hours - ) == 23 + expiry_delay = Timex.diff(expires_at, DateTime.utc_now(), :hours) + assert(expiry_delay in [23, 24]) assert_enqueued( worker: Pleroma.Workers.PurgeExpiredActivity, args: %{activity_id: id}, - scheduled_at: DateTime.add(DateTime.utc_now(), 1 * 60 * 60 * 24) + scheduled_at: expires_at ) end @@ -1405,7 +1406,7 @@ test "on pin removes deletion job, on unpin reschedule deletion" do %{conn: conn} = oauth_access(["write:accounts", "write:statuses"]) expires_in = 2 * 60 * 60 - expires_at = DateTime.add(DateTime.utc_now(), expires_in) + expires_at1 = DateTime.add(DateTime.utc_now(), expires_in) assert %{"id" => id} = conn @@ -1416,10 +1417,15 @@ test "on pin removes deletion job, on unpin reschedule deletion" do }) |> json_response_and_validate_schema(200) + activity = Activity.get_by_id(id) + {:ok, expires_at2, _} = DateTime.from_iso8601(activity.data["expires_at"]) + + assert Timex.compare(expires_at1, expires_at2, :minutes) == 0 + assert_enqueued( worker: Pleroma.Workers.PurgeExpiredActivity, args: %{activity_id: id}, - scheduled_at: expires_at + scheduled_at: expires_at2 ) assert %{"id" => ^id, "pinned" => true} = @@ -1431,7 +1437,7 @@ test "on pin removes deletion job, on unpin reschedule deletion" do refute_enqueued( worker: Pleroma.Workers.PurgeExpiredActivity, args: %{activity_id: id}, - scheduled_at: expires_at + scheduled_at: expires_at2 ) assert %{"id" => ^id, "pinned" => false} = @@ -1443,7 +1449,7 @@ test "on pin removes deletion job, on unpin reschedule deletion" do assert_enqueued( worker: Pleroma.Workers.PurgeExpiredActivity, args: %{activity_id: id}, - scheduled_at: expires_at + scheduled_at: expires_at2 ) end end @@ -1944,7 +1950,7 @@ test "expires_at is nil for another user" do |> json_response_and_validate_schema(:ok) {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) - assert DateTime.diff(expires_at, a_expires_at) == 0 + assert Timex.compare(expires_at, a_expires_at, :minutes) == 0 %{conn: conn} = oauth_access(["read:statuses"]) diff --git a/test/pleroma/web/metadata/providers/open_graph_test.exs b/test/pleroma/web/metadata/providers/open_graph_test.exs index 64ff19561..20afb64e5 100644 --- a/test/pleroma/web/metadata/providers/open_graph_test.exs +++ b/test/pleroma/web/metadata/providers/open_graph_test.exs @@ -9,6 +9,8 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) setup do: clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + setup do: clear_config([:restrict_unauthenticated, :profiles, :local]) + setup do: clear_config([:restrict_unauthenticated, :activities, :local]) test "it renders all supported types of attachments and skips unknown types" do user = insert(:user) @@ -188,4 +190,24 @@ test "video attachments have no image thumbnail with Preview Proxy disabled" do "http://localhost:4001/proxy/preview/LzAnlke-l5oZbNzWsrHfprX1rGw/aHR0cHM6Ly9wbGVyb21hLmdvdi9hYm91dC9qdWNoZS53ZWJt/juche.webm" ], []} in result end + + test "it does not render users if profiles are marked as restricted" do + clear_config([:restrict_unauthenticated, :profiles, :local], true) + + user = insert(:user) + + result = OpenGraph.build_tags(%{user: user}) + assert Enum.empty?(result) + end + + test "it does not activities users if they are marked as restricted" do + clear_config([:restrict_unauthenticated, :activities, :local], true) + + user = insert(:user) + note = insert(:note, data: %{"actor" => user.ap_id}) + + result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) + + assert {:meta, [property: "og:description", content: "Content cannot be displayed."], []} in result + end end diff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs index cd3f5eced..13fcd56ef 100644 --- a/test/pleroma/web/metadata/providers/twitter_card_test.exs +++ b/test/pleroma/web/metadata/providers/twitter_card_test.exs @@ -14,6 +14,8 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do alias Pleroma.Web.Metadata.Utils setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) + setup do: clear_config([:restrict_unauthenticated, :profiles, :local]) + setup do: clear_config([:restrict_unauthenticated, :activities, :local]) test "it renders twitter card for user info" do user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") @@ -28,6 +30,14 @@ test "it renders twitter card for user info" do ] end + test "it does not render twitter card for user info if it is restricted" do + clear_config([:restrict_unauthenticated, :profiles, :local], true) + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + res = TwitterCard.build_tags(%{user: user}) + + assert Enum.empty?(res) + end + test "it uses summary twittercard if post has no attachment" do user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) @@ -54,6 +64,16 @@ test "it uses summary twittercard if post has no attachment" do ] == result end + test "it does not summarise activities if they are marked as restricted" do + clear_config([:restrict_unauthenticated, :activities, :local], true) + user = insert(:user) + note = insert(:note, data: %{"actor" => user.ap_id}) + + result = TwitterCard.build_tags(%{object: note, activity_id: note.data["id"], user: user}) + + assert {:meta, [name: "twitter:description", content: "Content cannot be displayed."], []} in result + end + test "it uses summary as description if post has one" do user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) diff --git a/test/pleroma/web/plugs/http_security_plug_test.exs b/test/pleroma/web/plugs/http_security_plug_test.exs index bd8207ae0..009fb829c 100644 --- a/test/pleroma/web/plugs/http_security_plug_test.exs +++ b/test/pleroma/web/plugs/http_security_plug_test.exs @@ -7,8 +7,6 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do alias Plug.Conn - setup_all do: clear_config([Pleroma.Upload, :base_url], nil) - describe "http security enabled" do setup do: clear_config([:http_security, :enabled], true) @@ -98,51 +96,68 @@ test "it sets the Service-Worker-Allowed header", %{conn: conn} do test "media_proxy with base_url", %{conn: conn} do url = "https://example.com" clear_config([:media_proxy, :base_url], url) - assert_media_img_src(conn, url) + assert_media_img_src(conn, proxy: url) assert_connect_src(conn, url) end test "upload with base url", %{conn: conn} do url = "https://example2.com" clear_config([Pleroma.Upload, :base_url], url) - assert_media_img_src(conn, url) + assert_media_img_src(conn, upload: url) assert_connect_src(conn, url) end test "with S3 public endpoint", %{conn: conn} do url = "https://example3.com" clear_config([Pleroma.Uploaders.S3, :public_endpoint], url) - assert_media_img_src(conn, url) + assert_media_img_src(conn, s3: url) end test "with captcha endpoint", %{conn: conn} do clear_config([Pleroma.Captcha.Mock, :endpoint], "https://captcha.com") - assert_media_img_src(conn, "https://captcha.com") + assert_media_img_src(conn, captcha: "https://captcha.com") end test "with media_proxy whitelist", %{conn: conn} do clear_config([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"]) - assert_media_img_src(conn, "https://example7.com https://example6.com") + assert_media_img_src(conn, proxy_whitelist: "https://example7.com https://example6.com") end # TODO: delete after removing support bare domains for media proxy whitelist test "with media_proxy bare domains whitelist (deprecated)", %{conn: conn} do clear_config([:media_proxy, :whitelist], ["example4.com", "example5.com"]) - assert_media_img_src(conn, "example5.com example4.com") + assert_media_img_src(conn, proxy_whitelist: "example5.com example4.com") end test "with media_proxy blocklist", %{conn: conn} do clear_config([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"]) clear_config([:media_proxy, :blocklist], ["https://example8.com"]) - assert_media_img_src(conn, "https://example7.com https://example6.com") + assert_media_img_src(conn, proxy_whitelist: "https://example7.com https://example6.com") end end - defp assert_media_img_src(conn, url) do + defp maybe_concat(nil, b), do: b + defp maybe_concat(a, nil), do: a + defp maybe_concat(a, b), do: a <> " " <> b + + defp build_src_str(urls) do + urls[:proxy_whitelist] + |> maybe_concat(urls[:s3]) + |> maybe_concat(urls[:upload]) + |> maybe_concat(urls[:proxy]) + |> maybe_concat(urls[:captcha]) + end + + defp assert_media_img_src(conn, urls) do + urlstr = + [upload: "http://localhost", proxy: "http://localhost"] + |> Keyword.merge(urls) + |> build_src_str() + conn = get(conn, "/api/v1/instance") [csp] = Conn.get_resp_header(conn, "content-security-policy") - assert csp =~ "media-src 'self' #{url};" - assert csp =~ "img-src 'self' data: blob: #{url};" + assert csp =~ "media-src 'self' #{urlstr};" + assert csp =~ "img-src 'self' data: blob: #{urlstr};" end defp assert_connect_src(conn, url) do 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 d2bc7840f..5a94e4396 100644 --- a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs +++ b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs @@ -132,7 +132,7 @@ test "show follow page with error when user can not be fetched by `acct` link", |> html_response(200) assert response =~ "Error fetching user" - end) =~ "Object has been deleted" + end) =~ ":not_found" end end diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs index 169e9981c..3192ad3af 100644 --- a/test/pleroma/web/twitter_api/util_controller_test.exs +++ b/test/pleroma/web/twitter_api/util_controller_test.exs @@ -24,7 +24,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do describe "PUT /api/pleroma/notification_settings" do setup do: oauth_access(["write:accounts"]) - test "it updates notification settings", %{user: user, conn: conn} do + test "it updates notification settings via url paramters", %{user: user, conn: conn} do conn |> put( "/api/pleroma/notification_settings?#{URI.encode_query(%{block_from_strangers: true})}" @@ -39,6 +39,57 @@ test "it updates notification settings", %{user: user, conn: conn} do } == user.notification_settings end + test "it updates notification settings via JSON body params", %{user: user, conn: conn} do + conn + |> put_req_header("content-type", "application/json") + |> put( + "/api/pleroma/notification_settings", + %{"block_from_strangers" => true} + ) + |> json_response_and_validate_schema(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + block_from_strangers: true, + hide_notification_contents: false + } == user.notification_settings + end + + test "it updates notification settings via form data", %{user: user, conn: conn} do + conn + |> put_req_header("content-type", "multipart/form-data") + |> put( + "/api/pleroma/notification_settings", + %{:block_from_strangers => true} + ) + |> json_response_and_validate_schema(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + block_from_strangers: true, + hide_notification_contents: false + } == user.notification_settings + end + + test "it updates notification settings via urlencoded body", %{user: user, conn: conn} do + conn + |> put_req_header("content-type", "application/x-www-form-urlencoded") + |> put( + "/api/pleroma/notification_settings", + "block_from_strangers=true" + ) + |> json_response_and_validate_schema(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + block_from_strangers: true, + hide_notification_contents: false + } == user.notification_settings + end + test "it updates notification settings to enable hiding contents", %{user: user, conn: conn} do conn |> put( @@ -53,6 +104,27 @@ test "it updates notification settings to enable hiding contents", %{user: user, hide_notification_contents: true } == user.notification_settings end + + # we already test all body variants for block_from_strangers, so just one should suffice here + test "it updates notification settings to enable hiding contents via JSON body params", %{ + user: user, + conn: conn + } do + conn + |> put_req_header("content-type", "application/json") + |> put( + "/api/pleroma/notification_settings", + %{"hide_notification_contents" => true} + ) + |> json_response_and_validate_schema(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + block_from_strangers: false, + hide_notification_contents: true + } == user.notification_settings + end end describe "GET /api/pleroma/frontend_configurations" do 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 fe8301fa4..f792f20e6 100644 --- a/test/pleroma/web/web_finger/web_finger_controller_test.exs +++ b/test/pleroma/web/web_finger/web_finger_controller_test.exs @@ -46,8 +46,7 @@ test "Webfinger JRD" do assert response["subject"] == "acct:#{user.nickname}@localhost" assert response["aliases"] == [ - "https://hyrule.world/users/zelda", - "https://mushroom.kingdom/users/toad" + "https://hyrule.world/users/zelda" ] end @@ -104,7 +103,6 @@ test "Webfinger XML" do |> response(200) assert response =~ "https://hyrule.world/users/zelda" - assert response =~ "https://mushroom.kingdom/users/toad" end test "it returns 404 when user isn't found (XML)" do diff --git a/test/pleroma/workers/remote_fetcher_worker_test.exs b/test/pleroma/workers/remote_fetcher_worker_test.exs new file mode 100644 index 000000000..c30e773d4 --- /dev/null +++ b/test/pleroma/workers/remote_fetcher_worker_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.RemoteFetcherWorkerTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Workers.RemoteFetcherWorker + + @deleted_object_one "https://deleted-404.example.com/" + @deleted_object_two "https://deleted-410.example.com/" + @unauthorized_object "https://unauthorized.example.com/" + @depth_object "https://depth.example.com/" + + describe "RemoteFetcherWorker" do + setup do + Tesla.Mock.mock(fn + %{method: :get, url: @deleted_object_one} -> + %Tesla.Env{ + status: 404 + } + + %{method: :get, url: @deleted_object_two} -> + %Tesla.Env{ + status: 410 + } + + %{method: :get, url: @unauthorized_object} -> + %Tesla.Env{ + status: 403 + } + + %{method: :get, url: @depth_object} -> + %Tesla.Env{ + status: 200 + } + end) + end + + test "does not requeue a deleted object" do + assert {:discard, _} = + RemoteFetcherWorker.perform(%Oban.Job{ + args: %{"op" => "fetch_remote", "id" => @deleted_object_one} + }) + + assert {:discard, _} = + RemoteFetcherWorker.perform(%Oban.Job{ + args: %{"op" => "fetch_remote", "id" => @deleted_object_two} + }) + end + + test "does not requeue an unauthorized object" do + assert {:discard, _} = + RemoteFetcherWorker.perform(%Oban.Job{ + args: %{"op" => "fetch_remote", "id" => @unauthorized_object} + }) + end + + test "does not requeue an object that exceeded depth" do + clear_config([:instance, :federation_incoming_replies_max_depth], 0) + + assert {:discard, _} = + RemoteFetcherWorker.perform(%Oban.Job{ + args: %{"op" => "fetch_remote", "id" => @depth_object, "depth" => 1} + }) + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index e21b8fc1e..2a73a4ae6 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -559,16 +559,15 @@ 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"] - } + 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, diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index cc0e22af1..042e4110e 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -5,7 +5,16 @@ defmodule HttpRequestMock do require Logger - def activitypub_object_headers, do: [{"content-type", "application/activity+json"}] + def activitypub_object_headers, + do: [ + {"content-type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""} + ] + + # The Accept headers we genrate to be exact; AP spec only requires the first somewhere + @activitypub_accept_headers [ + {"accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}, + {"accept", "application/activity+json"} + ] def request( %Tesla.Env{ @@ -97,7 +106,7 @@ def get("https://mastodon.sdf.org/users/rinpatch/collections/featured", _, _, _) File.read!("test/fixtures/users_mock/masto_featured.json") |> String.replace("{{domain}}", "mastodon.sdf.org") |> String.replace("{{nickname}}", "rinpatch"), - headers: [{"content-type", "application/activity+json"}] + headers: activitypub_object_headers() }} end @@ -208,7 +217,7 @@ def get( "https://mst3k.interlinked.me/users/luciferMysticus", _, _, - [{"accept", "application/activity+json"}] + @activitypub_accept_headers ) do {:ok, %Tesla.Env{ @@ -231,7 +240,7 @@ def get( "https://hubzilla.example.org/channel/kaniini", _, _, - [{"accept", "application/activity+json"}] + @activitypub_accept_headers ) do {:ok, %Tesla.Env{ @@ -241,7 +250,7 @@ def get( }} end - def get("https://niu.moe/users/rye", _, _, [{"accept", "application/activity+json"}]) do + def get("https://niu.moe/users/rye", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, @@ -250,7 +259,7 @@ def get("https://niu.moe/users/rye", _, _, [{"accept", "application/activity+jso }} end - def get("https://n1u.moe/users/rye", _, _, [{"accept", "application/activity+json"}]) do + def get("https://n1u.moe/users/rye", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, @@ -270,7 +279,7 @@ def get("http://mastodon.example.org/users/admin/statuses/100787282858396771", _ }} end - def get("https://puckipedia.com/", _, _, [{"accept", "application/activity+json"}]) do + def get("https://puckipedia.com/", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, @@ -342,9 +351,12 @@ def get("https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34 }} end - def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, _, [ - {"accept", "application/activity+json"} - ]) do + def get( + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", + _, + _, + @activitypub_accept_headers + ) do {:ok, %Tesla.Env{ status: 200, @@ -353,7 +365,7 @@ def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, }} end - def get("https://mobilizon.org/@tcit", _, _, [{"accept", "application/activity+json"}]) do + def get("https://mobilizon.org/@tcit", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, @@ -416,9 +428,7 @@ def get( {:ok, %Tesla.Env{status: 404, body: ""}} end - def get("http://mastodon.example.org/users/relay", _, _, [ - {"accept", "application/activity+json"} - ]) do + def get("http://mastodon.example.org/users/relay", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, @@ -427,9 +437,7 @@ def get("http://mastodon.example.org/users/relay", _, _, [ }} end - def get("http://mastodon.example.org/users/gargron", _, _, [ - {"accept", "application/activity+json"} - ]) do + def get("http://mastodon.example.org/users/gargron", _, _, @activitypub_accept_headers) do {:error, :nxdomain} end @@ -620,7 +628,7 @@ def get("https://shitposter.club/notice/7369654", _, _, _) do }} end - def get("https://mstdn.io/users/mayuutann", _, _, [{"accept", "application/activity+json"}]) do + def get("https://mstdn.io/users/mayuutann", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, @@ -633,7 +641,7 @@ def get( "https://mstdn.io/users/mayuutann/statuses/99568293732299394", _, _, - [{"accept", "application/activity+json"}] + @activitypub_accept_headers ) do {:ok, %Tesla.Env{ @@ -779,7 +787,7 @@ def get( "http://gs.example.org:4040/index.php/user/1", _, _, - [{"accept", "application/activity+json"}] + @activitypub_accept_headers ) do {:ok, %Tesla.Env{status: 406, body: ""}} end @@ -966,7 +974,7 @@ def get("https://apfed.club/channel/indio", _, _, _) do }} end - def get("https://social.heldscal.la/user/23211", _, _, [{"accept", "application/activity+json"}]) do + def get("https://social.heldscal.la/user/23211", _, _, @activitypub_accept_headers) do {:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)} end @@ -1207,13 +1215,11 @@ def get("https://lm.kazv.moe/users/mewmew/collections/featured", _, _, _) do File.read!("test/fixtures/users_mock/masto_featured.json") |> String.replace("{{domain}}", "lm.kazv.moe") |> String.replace("{{nickname}}", "mewmew"), - headers: [{"content-type", "application/activity+json"}] + headers: activitypub_object_headers() }} end - def get("https://info.pleroma.site/activity.json", _, _, [ - {"accept", "application/activity+json"} - ]) do + def get("https://info.pleroma.site/activity.json", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, @@ -1226,9 +1232,7 @@ def get("https://info.pleroma.site/activity.json", _, _, _) do {:ok, %Tesla.Env{status: 404, body: ""}} end - def get("https://info.pleroma.site/activity2.json", _, _, [ - {"accept", "application/activity+json"} - ]) do + def get("https://info.pleroma.site/activity2.json", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, @@ -1241,9 +1245,7 @@ def get("https://info.pleroma.site/activity2.json", _, _, _) do {:ok, %Tesla.Env{status: 404, body: ""}} end - def get("https://info.pleroma.site/activity3.json", _, _, [ - {"accept", "application/activity+json"} - ]) do + def get("https://info.pleroma.site/activity3.json", _, _, @activitypub_accept_headers) do {:ok, %Tesla.Env{ status: 200, @@ -1319,6 +1321,25 @@ def get("https://skippers-bin.com/notes/7x9tmrp97i", _, _, _) do }} end + # A misskey quote + def get("https://misskey.io/notes/8vs6wxufd0", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://misskey.io/users/83ssedkv53", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/aimu@misskey.io.json"), + headers: activitypub_object_headers() + }} + end + def get("https://example.org/emoji/firedfox.png", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}} end