diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml
index 81c779b50..45cf7f93d 100644
--- a/.woodpecker/test.yml
+++ b/.woodpecker/test.yml
@@ -87,5 +87,5 @@ steps:
- mix ecto.create
- mix ecto.migrate
- mkdir -p test/tmp
- - mix test --preload-modules --exclude erratic --exclude federated --exclude mocked
- - mix test --preload-modules --only mocked
+ - mix test --preload-modules --exclude erratic --exclude federated --exclude mocked || mix test --failed
+ - mix test --preload-modules --only mocked || mix test --failed
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c743e5bd..74a925a3f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,11 @@ 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
+## 3.13.3
## BREAKING
- Minimum PostgreSQL version is raised to 12
+- Swagger UI moved from `/akkoma/swaggerui/` to `/pleroma/swaggerui/`
## Added
- Implement [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) (federation documentation)
@@ -17,6 +18,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Fixed
- Meilisearch: order of results returned from our REST API now actually matches how Meilisearch ranks results
+- Emoji are now federated as anonymous objects, fixing issues with
+ some strict servers e.g. rejecting e.g. remote emoji reactions
+- AP objects with additional JSON-LD profiles beyond ActivityStreams can now be fetched
+- Single-selection polls no longer expose the voter_count; MastoAPI demands it be null
+ and this confused some clients leading to vote distributions >100%
## Changed
- Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated.
diff --git a/config/test.exs b/config/test.exs
index 1df360ec7..556b9e78c 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -51,7 +51,8 @@
hostname: System.get_env("DB_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 50,
- queue_target: 5000
+ queue_target: 5000,
+ log: false
config :pleroma, :dangerzone, override_repo_pool_size: true
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
index 778ef08e2..ef5aa7d9e 100755
--- a/docker-entrypoint.sh
+++ b/docker-entrypoint.sh
@@ -11,4 +11,4 @@ echo "-- Running migrations..."
mix ecto.migrate
echo "-- Starting!"
-mix phx.server
+elixir --erl "+sbwt none +sbwtdcpu none +sbwtdio none" -S mix phx.server
diff --git a/docs/docs/configuration/cheatsheet.md b/docs/docs/configuration/cheatsheet.md
index 80f5c3577..916e1cc0c 100644
--- a/docs/docs/configuration/cheatsheet.md
+++ b/docs/docs/configuration/cheatsheet.md
@@ -338,7 +338,7 @@ config :pleroma, :frontends,
* `:primary` - The frontend that will be served at `/`
* `:admin` - The frontend that will be served at `/pleroma/admin`
-* `:swagger` - Config for developers to act as an API reference to be served at `/akkoma/swaggerui/` (trailing slash _needed_). Disabled by default.
+* `:swagger` - Config for developers to act as an API reference to be served at `/pleroma/swaggerui/` (trailing slash _needed_). Disabled by default.
* `:mastodon` - The mastodon-fe configuration. This shouldn't need to be changed. This is served at `/web` when installed.
### :static\_fe
diff --git a/docs/docs/configuration/frontend_management.md b/docs/docs/configuration/frontend_management.md
index bc5344826..8875ee279 100644
--- a/docs/docs/configuration/frontend_management.md
+++ b/docs/docs/configuration/frontend_management.md
@@ -60,4 +60,4 @@ config :pleroma, :frontends,
Then run the [pleroma.frontend cli task](../../administration/CLI_tasks/frontend) with the name of `swagger-ui` to install the distribution files.
-You will now be able to view documentation at `/akkoma/swaggerui`
+You will now be able to view documentation at `/pleroma/swaggerui`
diff --git a/docs/docs/installation/frontends.include b/docs/docs/installation/frontends.include
index 6da4018a9..094cfe4bd 100644
--- a/docs/docs/installation/frontends.include
+++ b/docs/docs/installation/frontends.include
@@ -6,7 +6,9 @@ probably install frontends.
These are no longer bundled with the distribution and need an extra
command to install.
-For most installations, the following will suffice:
+You **must** run frontend management tasks as the akkoma user,
+the same way you downloaded the build or cloned the git repo before.
+But otherwise, for most installations, the following will suffice:
=== "OTP"
```sh
@@ -28,4 +30,3 @@ For most installations, the following will suffice:
```
For more customised installations, refer to [Frontend Management](../../configuration/frontend_management)
-
diff --git a/installation/akkoma.service b/installation/akkoma.service
index 717693495..4945e108c 100644
--- a/installation/akkoma.service
+++ b/installation/akkoma.service
@@ -19,6 +19,9 @@ Environment="MIX_ENV=prod"
; Don't listen epmd on 0.0.0.0
Environment="ERL_EPMD_ADDRESS=127.0.0.1"
+; Don't busy wait
+Environment="ERL_AFLAGS=+sbwt none +sbwtdcpu none +sbwtdio none"
+
; Make sure that all paths fit your installation.
; Path to the home directory of the user running the Akkoma service.
Environment="HOME=/var/lib/akkoma"
diff --git a/installation/init.d/akkoma b/installation/init.d/akkoma
index bd17516f2..a03b494c0 100755
--- a/installation/init.d/akkoma
+++ b/installation/init.d/akkoma
@@ -1,23 +1,43 @@
#!/sbin/openrc-run
supervisor=supervise-daemon
-command_user=akkoma:akkoma
-command_background=1
-# Ask process to terminate within 30 seconds, otherwise kill it
-retry="SIGTERM/30/SIGKILL/5"
-pidfile="/var/run/akkoma.pid"
-directory=/opt/akkoma
-healthcheck_delay=60
-healthcheck_timer=30
no_new_privs="yes"
+pidfile="/var/run/akkoma.pid"
-: ${akkoma_port:-4000}
+# Ask process first to terminate itself within 60s, otherwise kill it
+retry="SIGTERM/60/SIGKILL/5"
-# Needs OpenRC >= 0.42
-#respawn_max=0
-#respawn_delay=5
+# if you really want to use start-stop-daemon instead,
+# also put the following in the config:
+# command_background=1
+
+# Adjust defaults as needed in /etc/conf.d/akkoma;
+# no need to directly edit the service file
+command_user="${command_user:-akkoma:akkoma}"
+directory="${directory:-/var/lib/akkoma/akkoma}"
+akkoma_port="${akkoma_port:-4000}"
+# whether to allow connecting a remote exlixir shell to the running Akkoma instance
+akkoma_console=${akkoma_console:-NO}
+
+output_log="${output_log:-/var/log/akkoma}"
+error_log="${error_log:-/var/log/akkoma}"
+
+# 0 means unlimited restarts
+respawn_max="${respawn_max:-0}"
+respawn_delay="${respawn_delay:-5}"
+# define respawn period to only count crashes within a
+# sliding time window towards respawn_max, e.g.:
+# respawn_period=2850
+
+healthcheck_delay="${healthcheck_delay:-60}"
+healthcheck_timer="${healthcheck_timer:-30}"
+
+MIX_ENV=prod
+ERL_EPMD_ADDRESS="${ERL_EPMD_ADDRESS:-127.0.0.1}"
+ERL_AFLAGS="${ERL_AFLAGS:-+sbwt none +sbwtdcpu none +sbwtdio none}"
+supervise_daemon_args="${supervise_daemon_args} --env MIX_ENV=${MIX_ENV}"
+supervise_daemon_args="${supervise_daemon_args} --env ERL_EPMD_ADDRESS=${ERL_EPMD_ADDRESS}"
+supervise_daemon_args="${supervise_daemon_args} --env ERL_AFLAGS='${ERL_AFLAGS}'"
-# put akkoma_console=YES in /etc/conf.d/akkoma if you want to be able to
-# connect to akkoma via an elixir console
if yesno "${akkoma_console}"; then
command=elixir
command_args="--name akkoma@127.0.0.1 --erl '-kernel inet_dist_listen_min 9001 inet_dist_listen_max 9001 inet_dist_use_interface {127,0,0,1}' -S mix phx.server"
@@ -31,13 +51,24 @@ else
command_args="phx.server"
fi
-export MIX_ENV=prod
-export ERL_EPMD_ADDRESS=127.0.0.1
-
depend() {
need nginx postgresql
}
+start_pre() {
+ # Ensure logfile ownership and perms are alright
+ checkpath --file --owner "$command_user" "$output_log" "$error_log" \
+ || eerror "Logfile(s) not owned by $command_user, or not a file!"
+ checkpath --writable "$output_log" "$error_log" \
+ || eerror "Logfile(s) not writable!"
+
+ # If a recompile is needed perform it with lowest prio
+ # (delaying the actual start) to avoid hogging too much
+ # CPU from other services
+ cd "$directory"
+ doas -u "${command_user%%:*}" env MIX_ENV="$MIX_ENV" nice -n 19 "$command" compile
+}
+
healthcheck() {
# put akkoma_health=YES in /etc/conf.d/akkoma if you want healthchecking
# and make sure you have curl installed
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index 12918dfff..aa8131254 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -111,7 +111,7 @@ def run(["get-packs" | args]) do
{:ok, _} =
:zip.unzip(binary_archive,
- cwd: pack_path,
+ cwd: to_charlist(pack_path),
file_list: files_to_unzip
)
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
index 142208854..e95320a07 100644
--- a/lib/pleroma/emoji/pack.ex
+++ b/lib/pleroma/emoji/pack.ex
@@ -125,7 +125,7 @@ def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"}
{:ok, _emoji_files} =
:zip.unzip(
to_charlist(file.path),
- [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
+ [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, to_charlist(tmp_dir)}]
)
{_, updated_pack} =
diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex
index dc9d55646..a309d8467 100644
--- a/lib/pleroma/frontend.ex
+++ b/lib/pleroma/frontend.ex
@@ -79,6 +79,10 @@ def unzip(zip, dest) do
new_file_path = Path.join(dest, path)
+ new_file_path
+ |> Path.dirname()
+ |> File.rm()
+
new_file_path
|> Path.dirname()
|> File.mkdir_p!()
diff --git a/lib/pleroma/keys.ex b/lib/pleroma/keys.ex
deleted file mode 100644
index 413861b15..000000000
--- a/lib/pleroma/keys.ex
+++ /dev/null
@@ -1,46 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Keys do
- # Native generation of RSA keys is only available since OTP 20+ and in default build conditions
- # We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
- try do
- _ = :public_key.generate_key({:rsa, 2048, 65_537})
-
- def generate_rsa_pem do
- key = :public_key.generate_key({:rsa, 2048, 65_537})
- entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
- pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
- {:ok, pem}
- end
- rescue
- _ ->
- def generate_rsa_pem do
- port = Port.open({:spawn, "openssl genrsa"}, [:binary])
-
- {:ok, pem} =
- receive do
- {^port, {:data, pem}} -> {:ok, pem}
- end
-
- Port.close(port)
-
- if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
- {:ok, pem}
- else
- :error
- end
- end
- end
-
- def keys_from_pem(pem) do
- with [private_key_code] <- :public_key.pem_decode(pem),
- private_key <- :public_key.pem_entry_decode(private_key_code),
- {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do
- {:ok, private_key, {:RSAPublicKey, modulus, exponent}}
- else
- error -> {:error, error}
- end
- end
-end
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index 37bc20e4d..7b1cc37bd 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -12,8 +12,6 @@ defmodule Pleroma.Object.Containment do
spoofing, therefore removal of object containment functions is NOT recommended.
"""
- alias Pleroma.Web.ActivityPub.Transmogrifier
-
def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end
@@ -50,16 +48,39 @@ def get_object(_) do
defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
defp compare_uris(_id_uri, _other_uri), do: :error
- defp compare_uris_exact(uri, uri), do: :ok
+ defp uri_strip_slash(%URI{path: path} = uri) when is_binary(path),
+ do: %{uri | path: String.replace_suffix(path, "/", "")}
- defp compare_uris_exact(%URI{} = id, %URI{} = other),
- do: compare_uris_exact(URI.to_string(id), URI.to_string(other))
+ defp uri_strip_slash(uri), do: uri
- defp compare_uris_exact(id_uri, other_uri)
- when is_binary(id_uri) and is_binary(other_uri) do
- norm_id = String.replace_suffix(id_uri, "/", "")
- norm_other = String.replace_suffix(other_uri, "/", "")
- if norm_id == norm_other, do: :ok, else: :error
+ # domain names are case-insensitive per spec (other parts of URIs aren’t necessarily)
+ defp uri_normalise_host(%URI{host: host} = uri) when is_binary(host),
+ do: %{uri | host: String.downcase(host, :ascii)}
+
+ defp uri_normalise_host(uri), do: uri
+
+ defp compare_uri_identities(uri, uri), do: :ok
+
+ defp compare_uri_identities(id_uri, other_uri) when is_binary(id_uri) and is_binary(other_uri),
+ do: compare_uri_identities(URI.parse(id_uri), URI.parse(other_uri))
+
+ defp compare_uri_identities(%URI{} = id, %URI{} = other) do
+ normid =
+ %{id | fragment: nil}
+ |> uri_strip_slash()
+ |> uri_normalise_host()
+
+ normother =
+ %{other | fragment: nil}
+ |> uri_strip_slash()
+ |> uri_normalise_host()
+
+ # Conversion back to binary avoids issues from non-normalised deprecated authority field
+ if URI.to_string(normid) == URI.to_string(normother) do
+ :ok
+ else
+ :error
+ end
end
@doc """
@@ -93,21 +114,13 @@ def contain_origin(id, %{"attributedTo" => actor} = params),
def contain_origin(_id, _data), do: :ok
@doc """
- Check whether the fetch URL (after redirects) exactly (sans tralining slash) matches either
- the canonical ActivityPub id or the objects url field (for display URLs from *key and Mastodon)
+ Check whether the fetch URL (after redirects) is the
+ same location the canonical ActivityPub id points to.
Since this is meant to be used for fetches, anonymous or transient objects are not accepted here.
"""
- def contain_id_to_fetch(url, %{"id" => id} = data) when is_binary(id) do
- with {:id, :error} <- {:id, compare_uris_exact(id, url)},
- # "url" can be a "Link" object and this is checked before full normalisation
- display_url <- Transmogrifier.fix_url(data)["url"],
- true <- display_url != nil do
- compare_uris_exact(display_url, url)
- else
- {:id, :ok} -> :ok
- _ -> :error
- end
+ def contain_id_to_fetch(url, %{"id" => id}) when is_binary(id) do
+ compare_uri_identities(url, id)
end
def contain_id_to_fetch(_url, _data), do: :error
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index 937026e04..11ed57ed5 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -116,7 +116,7 @@ defp reinject_object(%Object{} = object, new_data) do
@doc "Assumes object already is in our database and refetches from remote to update (e.g. for polls)"
def refetch_object(%Object{data: %{"id" => id}} = object) do
with {:local, false} <- {:local, Object.local?(object)},
- {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
+ {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id, true),
{:id, true} <- {:id, new_data["id"] == id},
{:ok, object} <- reinject_object(object, new_data) do
{:ok, object}
@@ -253,14 +253,17 @@ defp maybe_date_fetch(headers, date) do
end
end
- @doc "Fetches arbitrary remote object and performs basic safety and authenticity checks"
- def fetch_and_contain_remote_object_from_id(id)
+ @doc """
+ Fetches arbitrary remote object and performs basic safety and authenticity checks.
+ When the fetch URL is known to already be a canonical AP id, checks are stricter.
+ """
+ def fetch_and_contain_remote_object_from_id(id, is_ap_id \\ false)
- def fetch_and_contain_remote_object_from_id(%{"id" => id}),
- do: fetch_and_contain_remote_object_from_id(id)
+ def fetch_and_contain_remote_object_from_id(%{"id" => id}, is_ap_id),
+ do: fetch_and_contain_remote_object_from_id(id, is_ap_id)
- def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
- Logger.debug("Fetching object #{id} via AP")
+ def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do
+ Logger.debug("Fetching object #{id} via AP [ap_id=#{is_ap_id}]")
with {:valid_uri_scheme, true} <- {:valid_uri_scheme, String.starts_with?(id, "http")},
%URI{} = uri <- URI.parse(id),
@@ -270,18 +273,31 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
{:mrf_accept_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri)},
{:local_fetch, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)},
{:ok, final_id, body} <- get_object(id),
+ # a canonical ID shouldn't be a redirect
+ true <- !is_ap_id || final_id == id,
{:ok, data} <- safe_json_decode(body),
- {_, :ok} <- {:strict_id, Containment.contain_id_to_fetch(final_id, data)},
- {_, :ok} <- {:containment, Containment.contain_origin(final_id, data)} do
+ {_, :ok} <- {:containment, Containment.contain_origin(final_id, data)},
+ {_, _, :ok} <- {:strict_id, data["id"], Containment.contain_id_to_fetch(final_id, data)} do
unless Instances.reachable?(final_id) do
Instances.set_reachable(final_id)
end
{:ok, data}
else
- {:strict_id, _} = e ->
- log_fetch_error(id, e)
- {:error, :id_mismatch}
+ # E.g. Mastodon and *key serve the AP object directly under their display URLs without
+ # redirecting to their canonical location first, thus ids will expectedly differ.
+ # Similarly keys, either use a fragment ID and are a subobjects or a distinct ID
+ # but for compatibility are still a subobject presenting their owning actors ID at the toplevel.
+ # Refetching _once_ from the listed id, should yield a strict match afterwards.
+ {:strict_id, ap_id, _} = e ->
+ case is_ap_id do
+ false ->
+ fetch_and_contain_remote_object_from_id(ap_id, true)
+
+ true ->
+ log_fetch_error(id, e)
+ {:error, :id_mismatch}
+ end
{:mrf_reject_check, _} = e ->
log_fetch_error(id, e)
@@ -301,7 +317,7 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
{:containment, reason} ->
log_fetch_error(id, reason)
- {:error, reason}
+ {:error, {:containment, reason}}
{:error, e} ->
{:error, e}
@@ -311,25 +327,13 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
end
end
- def fetch_and_contain_remote_object_from_id(_id),
+ def fetch_and_contain_remote_object_from_id(_id, _is_ap_id),
do: {:error, :invalid_id}
- defp check_crossdomain_redirect(final_host, original_url)
-
# HOPEFULLY TEMPORARY
# Basically none of our Tesla mocks in tests set the (supposed to
# exist for Tesla proper) url parameter for their responses
# causing almost every fetch in test to fail otherwise
- if @mix_env == :test do
- defp check_crossdomain_redirect(nil, _) do
- {:cross_domain_redirect, false}
- end
- end
-
- defp check_crossdomain_redirect(final_host, original_url) do
- {:cross_domain_redirect, final_host != URI.parse(original_url).host}
- end
-
if @mix_env == :test do
defp get_final_id(nil, initial_url), do: initial_url
defp get_final_id("", initial_url), do: initial_url
@@ -355,10 +359,6 @@ def get_object(id) do
with {:ok, %{body: body, status: code, headers: headers, url: final_url}}
when code in 200..299 <-
HTTP.Backoff.get(id, headers),
- remote_host <-
- URI.parse(final_url).host,
- {:cross_domain_redirect, false} <-
- check_crossdomain_redirect(remote_host, id),
{:has_content_type, {_, content_type}} <-
{:has_content_type, List.keyfind(headers, "content-type", 0)},
{:parse_content_type, {:ok, "application", subtype, type_params}} <-
@@ -369,8 +369,12 @@ def get_object(id) do
{"activity+json", _} ->
{:ok, final_id, body}
- {"ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
- {:ok, final_id, body}
+ {"ld+json", %{"profile" => profiles}} ->
+ if "https://www.w3.org/ns/activitystreams" in String.split(profiles) do
+ {:ok, final_id, body}
+ else
+ {:error, {:content_type, content_type}}
+ end
_ ->
{:error, {:content_type, content_type}}
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index c4ac2c87e..bc3baf433 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -5,47 +5,25 @@
defmodule Pleroma.Signature do
@behaviour HTTPSignatures.Adapter
- alias Pleroma.EctoType.ActivityPub.ObjectValidators
- alias Pleroma.Keys
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
-
- @known_suffixes ["/publickey", "/main-key", "#key"]
+ alias Pleroma.User.SigningKey
+ require Logger
def key_id_to_actor_id(key_id) do
- uri =
- key_id
- |> URI.parse()
- |> Map.put(:fragment, nil)
- |> Map.put(:query, nil)
- |> remove_suffix(@known_suffixes)
+ case SigningKey.key_id_to_ap_id(key_id) do
+ nil ->
+ # hm, we SHOULD have gotten this in the pipeline before we hit here!
+ Logger.error("Could not figure out who owns the key #{key_id}")
+ {:error, :key_owner_not_found}
- maybe_ap_id = URI.to_string(uri)
-
- case ObjectValidators.ObjectID.cast(maybe_ap_id) do
- {:ok, ap_id} ->
- {:ok, ap_id}
-
- _ ->
- case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
- {:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
- _ -> {:error, maybe_ap_id}
- end
+ key ->
+ {:ok, key}
end
end
- defp remove_suffix(uri, [test | rest]) do
- if not is_nil(uri.path) and String.ends_with?(uri.path, test) do
- Map.put(uri, :path, String.replace(uri.path, test, ""))
- else
- remove_suffix(uri, rest)
- end
- end
-
- defp remove_suffix(uri, []), do: uri
-
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
+ {:ok, %SigningKey{}} <- SigningKey.get_or_fetch_by_key_id(kid),
{:ok, actor_id} <- key_id_to_actor_id(kid),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
@@ -57,8 +35,8 @@ def fetch_public_key(conn) do
def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
+ {:ok, %SigningKey{}} <- SigningKey.get_or_fetch_by_key_id(kid),
{:ok, actor_id} <- key_id_to_actor_id(kid),
- {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
@@ -67,9 +45,9 @@ def refetch_public_key(conn) do
end
end
- def sign(%User{keys: keys} = user, headers) do
- with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
- HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
+ def sign(%User{} = user, headers) do
+ with {:ok, private_key} <- SigningKey.private_key(user) do
+ HTTPSignatures.sign(private_key, SigningKey.local_key_id(user.ap_id), headers)
end
end
diff --git a/lib/pleroma/upload/filter/exiftool/strip_metadata.ex b/lib/pleroma/upload/filter/exiftool/strip_metadata.ex
index 912ff6a92..a2604a682 100644
--- a/lib/pleroma/upload/filter/exiftool/strip_metadata.ex
+++ b/lib/pleroma/upload/filter/exiftool/strip_metadata.ex
@@ -18,6 +18,7 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripMetadata do
# Formats not compatible with exiftool at this time
def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
+ def filter(%Pleroma.Upload{content_type: "image/bmp"}), do: {:ok, :noop}
def filter(%Pleroma.Upload{content_type: "image/svg+xml"}), do: {:ok, :noop}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 14414adc4..dfeab0410 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -25,7 +25,6 @@ defmodule Pleroma.User do
alias Pleroma.Hashtag
alias Pleroma.User.HashtagFollow
alias Pleroma.HTML
- alias Pleroma.Keys
alias Pleroma.MFA
alias Pleroma.Notification
alias Pleroma.Object
@@ -43,6 +42,7 @@ defmodule Pleroma.User do
alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe
alias Pleroma.Workers.BackgroundWorker
+ alias Pleroma.User.SigningKey
use Pleroma.Web, :verified_routes
@@ -101,7 +101,6 @@ defmodule Pleroma.User do
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
- field(:public_key, :string)
field(:ap_id, :string)
field(:avatar, :map, default: %{})
field(:local, :boolean, default: true)
@@ -222,6 +221,10 @@ defmodule Pleroma.User do
on_replace: :delete
)
+ # FOR THE FUTURE: We might want to make this a one-to-many relationship
+ # it's entirely possible right now, but we don't have a use case for it
+ has_one(:signing_key, SigningKey, foreign_key: :user_id)
+
timestamps()
end
@@ -440,6 +443,7 @@ defp fix_follower_address(params), do: params
def remote_user_changeset(struct \\ %User{local: false}, params) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
+ fields_limit = Config.get([:instance, :max_remote_account_fields], 0)
name =
case params[:name] do
@@ -453,10 +457,12 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)
+ |> Map.update(:fields, [], &Enum.take(&1, fields_limit))
|> truncate_fields_param()
|> fix_follower_address()
struct
+ |> Repo.preload(:signing_key)
|> cast(
params,
[
@@ -466,7 +472,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:inbox,
:shared_inbox,
:nickname,
- :public_key,
:avatar,
:ap_enabled,
:banner,
@@ -495,6 +500,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|> validate_required([:ap_id])
|> validate_required([:name], trim: false)
|> unique_constraint(:nickname)
+ |> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, required: false)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
@@ -526,7 +532,6 @@ def update_changeset(struct, params \\ %{}) do
:name,
:emoji,
:avatar,
- :public_key,
:inbox,
:shared_inbox,
:is_locked,
@@ -570,6 +575,7 @@ def update_changeset(struct, params \\ %{}) do
:pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
)
+ |> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, requred: false)
|> validate_fields(false, struct)
end
@@ -828,8 +834,10 @@ def put_following_and_follower_and_featured_address(changeset) do
end
defp put_private_key(changeset) do
- {:ok, pem} = Keys.generate_rsa_pem()
- put_change(changeset, :keys, pem)
+ ap_id = get_field(changeset, :ap_id)
+
+ changeset
+ |> put_assoc(:signing_key, SigningKey.generate_local_keys(ap_id))
end
defp autofollow_users(user) do
@@ -1146,7 +1154,8 @@ def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
was_superuser_before_update = User.superuser?(user)
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
- set_cache(user)
+ user
+ |> set_cache()
end
|> maybe_remove_report_notifications(was_superuser_before_update)
end
@@ -1624,8 +1633,12 @@ def blocks_user?(%User{} = user, %User{} = target) do
def blocks_user?(_, _), do: false
- def blocks_domain?(%User{} = user, %User{} = target) do
- %{host: host} = URI.parse(target.ap_id)
+ def blocks_domain?(%User{} = user, %User{ap_id: ap_id}) do
+ blocks_domain?(user, ap_id)
+ end
+
+ def blocks_domain?(%User{} = user, url) when is_binary(url) do
+ %{host: host} = URI.parse(url)
Enum.member?(user.domain_blocks, host)
# TODO: functionality should probably be changed such that subdomains block as well,
# but as it stands, this just hecks up the relationships endpoint
@@ -2047,24 +2060,16 @@ defp create_service_actor(uri, nickname) do
|> set_cache()
end
- def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
- key =
- public_key_pem
- |> :public_key.pem_decode()
- |> hd()
- |> :public_key.pem_entry_decode()
-
- {:ok, key}
- end
-
- def public_key(_), do: {:error, "key not found"}
+ defdelegate public_key(user), to: SigningKey
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
- {:ok, public_key} <- public_key(user) do
+ {:ok, public_key} <- SigningKey.public_key(user) do
{:ok, public_key}
else
- _ -> :error
+ e ->
+ Logger.error("Could not get public key for #{ap_id}.\n#{inspect(e)}")
+ {:error, e}
end
end
diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex
new file mode 100644
index 000000000..f25489068
--- /dev/null
+++ b/lib/pleroma/user/signing_key.ex
@@ -0,0 +1,262 @@
+defmodule Pleroma.User.SigningKey do
+ use Ecto.Schema
+ import Ecto.Query
+ import Ecto.Changeset
+ require Pleroma.Constants
+ alias Pleroma.User
+ alias Pleroma.Repo
+
+ require Logger
+
+ @primary_key false
+ schema "signing_keys" do
+ belongs_to(:user, Pleroma.User, type: FlakeId.Ecto.CompatType)
+ field :public_key, :string
+ field :private_key, :string
+ # This is an arbitrary field given by the remote instance
+ field :key_id, :string, primary_key: true
+ timestamps()
+ end
+
+ def load_key(%User{} = user) do
+ user
+ |> Repo.preload(:signing_key)
+ end
+
+ def key_id_of_local_user(%User{local: true} = user) do
+ case Repo.preload(user, :signing_key) do
+ %User{signing_key: %__MODULE__{key_id: key_id}} -> key_id
+ _ -> nil
+ end
+ end
+
+ @spec remote_changeset(__MODULE__, map) :: Changeset.t()
+ def remote_changeset(%__MODULE__{} = signing_key, attrs) do
+ signing_key
+ |> cast(attrs, [:public_key, :key_id])
+ |> validate_required([:public_key, :key_id])
+ end
+
+ @spec key_id_to_user_id(String.t()) :: String.t() | nil
+ @doc """
+ Given a key ID, return the user ID associated with that key.
+ Returns nil if the key ID is not found.
+ """
+ def key_id_to_user_id(key_id) do
+ from(sk in __MODULE__, where: sk.key_id == ^key_id)
+ |> select([sk], sk.user_id)
+ |> Repo.one()
+ end
+
+ @spec key_id_to_ap_id(String.t()) :: String.t() | nil
+ @doc """
+ Given a key ID, return the AP ID associated with that key.
+ Returns nil if the key ID is not found.
+ """
+ def key_id_to_ap_id(key_id) do
+ Logger.debug("Looking up key ID: #{key_id}")
+
+ result =
+ from(sk in __MODULE__, where: sk.key_id == ^key_id)
+ |> join(:inner, [sk], u in User, on: sk.user_id == u.id)
+ |> select([sk, u], %{user: u})
+ |> Repo.one()
+
+ case result do
+ %{user: %User{ap_id: ap_id}} -> ap_id
+ _ -> nil
+ end
+ end
+
+ @spec generate_rsa_pem() :: {:ok, binary()}
+ @doc """
+ Generate a new RSA private key and return it as a PEM-encoded string.
+ """
+ def generate_rsa_pem do
+ key = :public_key.generate_key({:rsa, 2048, 65_537})
+ entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
+ pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
+ {:ok, pem}
+ end
+
+ @spec generate_local_keys(String.t()) :: {:ok, Changeset.t()} | {:error, String.t()}
+ @doc """
+ Generate a new RSA key pair and create a changeset for it
+ """
+ def generate_local_keys(ap_id) do
+ {:ok, private_pem} = generate_rsa_pem()
+ {:ok, local_pem} = private_pem_to_public_pem(private_pem)
+
+ %__MODULE__{}
+ |> change()
+ |> put_change(:public_key, local_pem)
+ |> put_change(:private_key, private_pem)
+ |> put_change(:key_id, local_key_id(ap_id))
+ end
+
+ @spec local_key_id(String.t()) :: String.t()
+ @doc """
+ Given an AP ID, return the key ID for the local user.
+ """
+ def local_key_id(ap_id) do
+ ap_id <> "#main-key"
+ end
+
+ @spec private_pem_to_public_pem(binary) :: {:ok, binary()} | {:error, String.t()}
+ @doc """
+ Given a private key in PEM format, return the corresponding public key in PEM format.
+ """
+ def private_pem_to_public_pem(private_pem) do
+ [private_key_code] = :public_key.pem_decode(private_pem)
+ private_key = :public_key.pem_entry_decode(private_key_code)
+ {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
+ public_key = {:RSAPublicKey, modulus, exponent}
+ public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
+ {:ok, :public_key.pem_encode([public_key])}
+ end
+
+ @spec public_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
+ @doc """
+ Given a user, return the public key for that user in binary format.
+ """
+ def public_key(%User{} = user) do
+ case Repo.preload(user, :signing_key) do
+ %User{signing_key: %__MODULE__{public_key: public_key_pem}} ->
+ key =
+ public_key_pem
+ |> :public_key.pem_decode()
+ |> hd()
+ |> :public_key.pem_entry_decode()
+
+ {:ok, key}
+
+ _ ->
+ {:error, "key not found"}
+ end
+ end
+
+ def public_key(_), do: {:error, "key not found"}
+
+ def public_key_pem(%User{} = user) do
+ case Repo.preload(user, :signing_key) do
+ %User{signing_key: %__MODULE__{public_key: public_key_pem}} -> {:ok, public_key_pem}
+ _ -> {:error, "key not found"}
+ end
+ end
+
+ def public_key_pem(_e) do
+ {:error, "key not found"}
+ end
+
+ @spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
+ @doc """
+ Given a user, return the private key for that user in binary format.
+ """
+ def private_key(%User{} = user) do
+ case Repo.preload(user, :signing_key) do
+ %{signing_key: %__MODULE__{private_key: private_key_pem}} ->
+ key =
+ private_key_pem
+ |> :public_key.pem_decode()
+ |> hd()
+ |> :public_key.pem_entry_decode()
+
+ {:ok, key}
+
+ _ ->
+ {:error, "key not found"}
+ end
+ end
+
+ @spec get_or_fetch_by_key_id(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
+ @doc """
+ Given a key ID, return the signing key associated with that key.
+ Will either return the key if it exists locally, or fetch it from the remote instance.
+ """
+ def get_or_fetch_by_key_id(key_id) do
+ case key_id_to_user_id(key_id) do
+ nil ->
+ fetch_remote_key(key_id)
+
+ user_id ->
+ {:ok, Repo.get_by(__MODULE__, user_id: user_id)}
+ end
+ end
+
+ @spec fetch_remote_key(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
+ @doc """
+ Fetch a remote key by key ID.
+ Will send a request to the remote instance to get the key ID.
+ This request should, at the very least, return a user ID and a public key object.
+ Though bear in mind that some implementations (looking at you, pleroma) may require a signature for this request.
+ This has the potential to create an infinite loop if the remote instance requires a signature to fetch the key...
+ So if we're rejected, we should probably just give up.
+ """
+ def fetch_remote_key(key_id) do
+ Logger.debug("Fetching remote key: #{key_id}")
+ resp = Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(key_id)
+
+ case resp do
+ {:ok, _body} ->
+ case handle_signature_response(resp) do
+ {:ok, ap_id, public_key_pem} ->
+ Logger.debug("Fetched remote key: #{ap_id}")
+ # fetch the user
+ {:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
+ # store the key
+ key = %__MODULE__{
+ user_id: user.id,
+ public_key: public_key_pem,
+ key_id: key_id
+ }
+
+ Repo.insert(key, on_conflict: :replace_all, conflict_target: :key_id)
+
+ e ->
+ Logger.debug("Failed to fetch remote key: #{inspect(e)}")
+ {:error, "Could not fetch key"}
+ end
+
+ _ ->
+ Logger.debug("Failed to fetch remote key: #{inspect(resp)}")
+ {:error, "Could not fetch key"}
+ end
+ end
+
+ # Take the response from the remote instance and extract the key details
+ # will check if the key ID matches the owner of the key, if not, error
+ defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do
+ if ap_id !== public_key["owner"] do
+ {:error, "Key ID does not match owner"}
+ else
+ %{"publicKeyPem" => public_key_pem} = public_key
+ {:ok, ap_id, public_key_pem}
+ end
+ end
+
+ defp handle_signature_response({:ok, body}) do
+ case body do
+ %{
+ "type" => "CryptographicKey",
+ "publicKeyPem" => public_key_pem,
+ "owner" => ap_id
+ } ->
+ {:ok, ap_id, public_key_pem}
+
+ # for when we get a subset of the user object
+ %{
+ "id" => _user_id,
+ "publicKey" => _public_key,
+ "type" => actor_type
+ }
+ when actor_type in Pleroma.Constants.actor_types() ->
+ extract_key_details(body)
+
+ %{"error" => error} ->
+ {:error, error}
+ end
+ end
+
+ defp handle_signature_response({:error, e}), do: {:error, e}
+ defp handle_signature_response(other), do: {:error, "Could not fetch key: #{inspect(other)}"}
+end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index c87072300..9b28e64d9 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1547,6 +1547,17 @@ defp normalize_attachment(%{} = attachment), do: [attachment]
defp normalize_attachment(attachment) when is_list(attachment), do: attachment
defp normalize_attachment(_), do: []
+ defp maybe_make_public_key_object(data) do
+ if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
+ %{
+ public_key: data["publicKey"]["publicKeyPem"],
+ key_id: data["publicKey"]["id"]
+ }
+ else
+ nil
+ end
+ end
+
defp object_to_user_data(data, additional) do
fields =
data
@@ -1578,9 +1589,16 @@ defp object_to_user_data(data, additional) do
featured_address = data["featured"]
{:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address)
- public_key =
- if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
- data["publicKey"]["publicKeyPem"]
+ # first, check that the owner is correct
+ signing_key =
+ if data["id"] !== data["publicKey"]["owner"] do
+ Logger.error(
+ "Owner of the public key is not the same as the actor - not saving the public key."
+ )
+
+ nil
+ else
+ maybe_make_public_key_object(data)
end
shared_inbox =
@@ -1624,7 +1642,7 @@ defp object_to_user_data(data, additional) do
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: also_known_as,
- public_key: public_key,
+ signing_key: signing_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
pinned_objects: pinned_objects,
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 6642f7771..9c6b3655d 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -60,7 +60,26 @@ defp relay_active?(conn, _) do
end
end
- def user(conn, %{"nickname" => nickname}) do
+ @doc """
+ Render the user's AP data
+ WARNING: we cannot actually check if the request has a fragment! so let's play defensively
+ - IF we have a valid signature, serve full user
+ - IF we do not, and authorized_fetch_mode is enabled, serve the key only
+ - OTHERWISE, serve the full actor (since we don't need to worry about the signature)
+ """
+ def user(%{assigns: %{valid_signature: true}} = conn, params) do
+ render_full_user(conn, params)
+ end
+
+ def user(conn, params) do
+ if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
+ render_key_only_user(conn, params)
+ else
+ render_full_user(conn, params)
+ end
+ end
+
+ defp render_full_user(conn, %{"nickname" => nickname}) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
@@ -72,6 +91,18 @@ def user(conn, %{"nickname" => nickname}) do
end
end
+ def render_key_only_user(conn, %{"nickname" => nickname}) do
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
+ conn
+ |> put_resp_content_type("application/activity+json")
+ |> put_view(UserView)
+ |> render("keys.json", %{user: user})
+ else
+ nil -> {:error, :not_found}
+ %{local: false} -> {:error, :not_found}
+ end
+ end
+
def object(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
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 adb291a55..b80068e37 100644
--- a/lib/pleroma/web/activity_pub/object_validators/user_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/user_validator.ex
@@ -14,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
alias Pleroma.Object.Containment
- alias Pleroma.Signature
require Pleroma.Constants
@@ -23,8 +22,7 @@ def validate(object, meta)
def validate(%{"type" => type, "id" => _id} = data, meta)
when type in Pleroma.Constants.actor_types() do
- with :ok <- validate_pubkey(data),
- :ok <- validate_inbox(data),
+ with :ok <- validate_inbox(data),
:ok <- contain_collection_origin(data) do
{:ok, data, meta}
else
@@ -35,33 +33,6 @@ def validate(%{"type" => type, "id" => _id} = data, meta)
def validate(_, _), do: {:error, "Not a user object"}
- defp mabye_validate_owner(nil, _actor), do: :ok
- defp mabye_validate_owner(actor, actor), do: :ok
- defp mabye_validate_owner(_owner, _actor), do: :error
-
- defp validate_pubkey(
- %{"id" => id, "publicKey" => %{"id" => pk_id, "publicKeyPem" => _key}} = data
- )
- when id != nil do
- with {_, {:ok, kactor}} <- {:key, Signature.key_id_to_actor_id(pk_id)},
- true <- id == kactor,
- :ok <- mabye_validate_owner(Map.get(data, "owner"), id) do
- :ok
- else
- {:key, _} ->
- {:error, "Unable to determine actor id from key id"}
-
- false ->
- {:error, "Key id does not relate to user id"}
-
- _ ->
- {:error, "Actor does not own its public key"}
- end
- end
-
- # pubkey is optional atm
- defp validate_pubkey(_data), do: :ok
-
defp validate_inbox(%{"id" => id, "inbox" => inbox}) do
case Containment.same_origin(id, inbox) do
:ok -> :ok
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index ca5e85f2e..75c1f0f0c 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -951,7 +951,7 @@ defp build_emoji_tag({name, url}) do
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
- "id" => url
+ "id" => nil
}
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 47b8e37e5..2ca31fc3c 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view
- alias Pleroma.Keys
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
@@ -33,9 +32,7 @@ def render("endpoints.json", %{user: %User{local: true} = _user}) do
def render("endpoints.json", _), do: %{}
def render("service.json", %{user: user}) do
- {:ok, _, public_key} = Keys.keys_from_pem(user.keys)
- public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
- public_key = :public_key.pem_encode([public_key])
+ {:ok, public_key} = User.SigningKey.public_key_pem(user)
endpoints = render("endpoints.json", %{user: user})
@@ -52,7 +49,7 @@ def render("service.json", %{user: user}) do
"url" => user.ap_id,
"manuallyApprovesFollowers" => false,
"publicKey" => %{
- "id" => "#{user.ap_id}#main-key",
+ "id" => User.SigningKey.local_key_id(user.ap_id),
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
@@ -70,9 +67,12 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
def render("user.json", %{user: user}) do
- {:ok, _, public_key} = Keys.keys_from_pem(user.keys)
- public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
- public_key = :public_key.pem_encode([public_key])
+ public_key =
+ case User.SigningKey.public_key_pem(user) do
+ {:ok, public_key} -> public_key
+ _ -> nil
+ end
+
user = User.sanitize_html(user)
endpoints = render("endpoints.json", %{user: user})
@@ -97,7 +97,7 @@ def render("user.json", %{user: user}) do
"url" => user.ap_id,
"manuallyApprovesFollowers" => user.is_locked,
"publicKey" => %{
- "id" => "#{user.ap_id}#main-key",
+ "id" => User.SigningKey.local_key_id(user.ap_id),
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
@@ -116,6 +116,20 @@ def render("user.json", %{user: user}) do
|> Map.merge(Utils.make_json_ld_header())
end
+ def render("keys.json", %{user: user}) do
+ {:ok, public_key} = User.SigningKey.public_key_pem(user)
+
+ %{
+ "id" => user.ap_id,
+ "publicKey" => %{
+ "id" => User.SigningKey.key_id_of_local_user(user),
+ "owner" => user.ap_id,
+ "publicKeyPem" => public_key
+ }
+ }
+ |> Map.merge(Utils.make_json_ld_header())
+ end
+
def render("following.json", %{user: user, page: page} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
showing_count = showing_items || !user.hide_follows_count
diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex
index 943ad8bd4..13a5e8be2 100644
--- a/lib/pleroma/web/api_spec/schemas/poll.ex
+++ b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -32,7 +32,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
},
voters_count: %Schema{
type: :integer,
- description: "How many unique accounts have voted. Number."
+ nullable: true,
+ description:
+ "How many unique accounts have voted for a multi-selection poll. Number, or null if single-selection poll."
},
voted: %Schema{
type: :boolean,
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 6628fcaf3..3c9fcb48c 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -66,10 +66,10 @@ defmodule Pleroma.Web.Endpoint do
}
)
- plug(Plug.Static.IndexHtml, at: "/akkoma/swaggerui")
+ plug(Plug.Static.IndexHtml, at: "/pleroma/swaggerui/")
plug(Pleroma.Web.Plugs.FrontendStatic,
- at: "/akkoma/swaggerui",
+ at: "/pleroma/swaggerui",
frontend_type: :swagger,
gzip: true,
if: &Pleroma.Web.Swagger.ui_enabled?/0,
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index aa6443754..411cbd15a 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -19,7 +19,7 @@ def render("show.json", %{object: object, multiple: multiple, options: options}
expired: expired,
multiple: multiple,
votes_count: votes_count,
- voters_count: voters_count(object),
+ voters_count: voters_count(multiple, object),
options: options,
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
}
@@ -68,11 +68,19 @@ defp options_and_votes_count(options) do
end)
end
- defp voters_count(%{data: %{"voters" => voters}}) when is_list(voters) do
+ defp voters_count(false, _poll_data) do
+ # Mastodon always sets voter count to "null" unless multiple options were selectable
+ # Some clients may rely on this to detect multiple selection polls and it can mess
+ # up percentages for some clients if we never got a correct remote voter count and
+ # only count local voters here; see https://akkoma.dev/AkkomaGang/akkoma/issues/190
+ nil
+ end
+
+ defp voters_count(_multiple, %{data: %{"voters" => voters}}) when is_list(voters) do
length(voters)
end
- defp voters_count(_), do: 0
+ defp voters_count(_, _), do: 0
defp voted_and_own_votes(%{object: object} = params, options) do
if params[:for] do
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
index e762fcad8..66cff7aaa 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
@@ -52,6 +52,14 @@ defp filter_allowed_user_by_ap_id(ap_ids, excluded_ap_ids) do
end)
end
+ defp filter_allowed_users_by_domain(ap_ids, %User{} = for_user) do
+ Enum.reject(ap_ids, fn ap_id ->
+ User.blocks_domain?(for_user, ap_id)
+ end)
+ end
+
+ defp filter_allowed_users_by_domain(ap_ids, nil), do: ap_ids
+
def filter_allowed_users(reactions, user, with_muted) do
exclude_ap_ids =
if is_nil(user) do
@@ -62,7 +70,10 @@ def filter_allowed_users(reactions, user, with_muted) do
end
filter_emoji = fn emoji, users, url ->
- case filter_allowed_user_by_ap_id(users, exclude_ap_ids) do
+ users
+ |> filter_allowed_user_by_ap_id(exclude_ap_ids)
+ |> filter_allowed_users_by_domain(user)
+ |> case do
[] -> nil
users -> {emoji, users, url}
end
diff --git a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex
new file mode 100644
index 000000000..5b090f289
--- /dev/null
+++ b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex
@@ -0,0 +1,32 @@
+defmodule Pleroma.Web.Plugs.EnsureUserPublicKeyPlug do
+ @moduledoc """
+ This plug will attempt to pull in a user's public key if we do not have it.
+ We _should_ be able to request the URL from the key URL...
+ """
+
+ alias Pleroma.User
+
+ def init(options), do: options
+
+ def call(conn, _opts) do
+ key_id = key_id_from_conn(conn)
+
+ unless is_nil(key_id) do
+ User.SigningKey.get_or_fetch_by_key_id(key_id)
+ # now we SHOULD have the user that owns the key locally. maybe.
+ # if we don't, we'll error out when we try to validate.
+ end
+
+ conn
+ end
+
+ defp key_id_from_conn(conn) do
+ case HTTPSignatures.signature_for_conn(conn) do
+ %{"keyId" => key_id} when is_binary(key_id) ->
+ key_id
+
+ _ ->
+ nil
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
index e3ae99636..06527cada 100644
--- a/lib/pleroma/web/plugs/http_signature_plug.ex
+++ b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -139,12 +139,17 @@ defp maybe_require_signature(
defp maybe_require_signature(conn), do: conn
defp signature_host(conn) do
- with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
- {:ok, actor_id} <- Signature.key_id_to_actor_id(kid) do
+ with {:key_id, %{"keyId" => kid}} <- {:key_id, HTTPSignatures.signature_for_conn(conn)},
+ {:actor_id, {:ok, actor_id}} <- {:actor_id, Signature.key_id_to_actor_id(kid)} do
actor_id
else
- e ->
- {:error, e}
+ {:key_id, e} ->
+ Logger.error("Failed to extract key_id from signature: #{inspect(e)}")
+ nil
+
+ {:actor_id, e} ->
+ Logger.error("Failed to extract actor_id from signature: #{inspect(e)}")
+ nil
end
end
end
diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
index a73def682..0d5cb9eab 100644
--- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
+++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
@@ -4,7 +4,6 @@
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
alias Pleroma.Helpers.AuthHelper
- alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
@@ -33,7 +32,7 @@ def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = con
|> assign(:valid_signature, false)
# remove me once testsuite uses mapped capabilities instead of what we do now
- {:user, nil} ->
+ {:user, _} ->
Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
@@ -68,7 +67,8 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
- only_permit_user_routes(conn)
+ conn
+ |> assign(:valid_signature, false)
_ ->
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
@@ -82,33 +82,34 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
# no signature at all
def call(conn, _opts), do: conn
- defp only_permit_user_routes(%{path_info: ["users", _]} = conn) do
- conn
- |> assign(:limited_ap, true)
- end
-
- defp only_permit_user_routes(conn) do
- conn
- |> assign(:valid_signature, false)
- end
-
defp key_id_from_conn(conn) do
- with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
- {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
- ap_id
- else
+ case HTTPSignatures.signature_for_conn(conn) do
+ %{"keyId" => key_id} when is_binary(key_id) ->
+ key_id
+
_ ->
nil
end
end
defp user_from_key_id(conn) do
- with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
- {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do
+ with {:key_id, key_id} when is_binary(key_id) <- {:key_id, key_id_from_conn(conn)},
+ {:mapped_ap_id, ap_id} when is_binary(ap_id) <-
+ {:mapped_ap_id, User.SigningKey.key_id_to_ap_id(key_id)},
+ {:user_fetch, {:ok, %User{} = user}} <- {:user_fetch, User.get_or_fetch_by_ap_id(ap_id)} do
user
else
- _ ->
- nil
+ {:key_id, nil} ->
+ Logger.debug("Failed to map identity from signature (no key ID)")
+ {:key_id, nil}
+
+ {:mapped_ap_id, nil} ->
+ Logger.debug("Failed to map identity from signature (could not map key ID to AP ID)")
+ {:mapped_ap_id, nil}
+
+ {:user_fetch, {:error, _}} ->
+ Logger.debug("Failed to map identity from signature (lookup failure)")
+ {:user_fetch, nil}
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 49ab3540b..c227d0d4a 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -144,7 +144,14 @@ defmodule Pleroma.Web.Router do
})
end
+ pipeline :optional_http_signature do
+ plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
+ plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
+ plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
+ end
+
pipeline :http_signature do
+ plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug)
@@ -745,7 +752,7 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
- pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
+ pipe_through([:accepts_html_xml_json, :optional_http_signature, :static_fe])
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
diff --git a/mix.exs b/mix.exs
index 697c050c8..1208e7221 100644
--- a/mix.exs
+++ b/mix.exs
@@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
- version: version("3.13.2"),
+ version: version("3.13.3"),
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(),
@@ -180,7 +180,7 @@ defp deps do
{:remote_ip, "~> 1.1.0"},
{:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
- ref: "90f6ce7672f70f56708792a98d98bd05176c9176"},
+ ref: "6630c42aaaab124e697b4e513190c89d8b64e410"},
{:restarter, path: "./restarter"},
{:majic,
git: "https://akkoma.dev/AkkomaGang/majic.git",
@@ -200,7 +200,7 @@ defp deps do
## dev & test
{:ex_doc, "~> 0.30", only: :dev, runtime: false},
- {:ex_machina, "~> 2.7", only: :test},
+ {:ex_machina, "~> 2.8", only: :test},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.8", only: :test},
{:excoveralls, "0.16.1", only: :test},
diff --git a/mix.lock b/mix.lock
index d703f6ce9..d9ff0e8e0 100644
--- a/mix.lock
+++ b/mix.lock
@@ -7,28 +7,29 @@
"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.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
+ "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "6630c42aaaab124e697b4e513190c89d8b64e410", [ref: "6630c42aaaab124e697b4e513190c89d8b64e410"]},
+ "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
"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"},
+ "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"},
"concurrent_limiter": {:git, "https://akkoma.dev/AkkomaGang/concurrent-limiter.git", "a9e0b3d64574bdba761f429bb4fba0cf687b3338", [ref: "a9e0b3d64574bdba761f429bb4fba0cf687b3338"]},
+ "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
- "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
+ "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [: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", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
- "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
+ "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"},
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
"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.8.0", "440719cd74f09b3f01c84455707a2c3972b725c513808e68eb6c5b0ab82bf523", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 0.18.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "f1512812dc196bcb932a96c82e55f69b543dc125e9d39f5e3631a9c4ec65ef12"},
+ "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.2", "79350a53246ac5ec27326d208496aebceb77fa82a91744f66a9154560f0759d3", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0 and < 0.20.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "6149c1c4a5ba6602a76cb09ee7a269eb60dab9694a1dbbb797f032555212de75"},
"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.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
@@ -36,8 +37,8 @@
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"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.4", "86c5bb870a49e0ab6f5aa5dd58cf505f09d2624ebe17530db3c1b61c88a673af", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82bd0091bb9a5bb190139599f922ff3fc7aebcca4374d65c99c4e23aa6d1625"},
- "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"},
+ "ex_aws": {:hex, :ex_aws, "2.5.6", "6f642e0f82eff10a9b470044f084b81a791cf15b393d647ea5f3e65da2794e3d", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c69eec59e31fdd89d0beeb1d97e16518dd1b23ad95b3d5c9f1dcfec23d97f960"},
+ "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.4", "87aaf4a2f24a48f516d7f5aaced9d128dd5d0f655c4431f9037a11a85c71109c", [: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", "c06e7f68b33f7c0acba1361dbd951c79661a28f85aa2e0582990fccca4425355"},
"ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"},
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
"ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"},
@@ -46,10 +47,11 @@
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
"fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"},
"fast_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_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
+ "file_ex": {:git, "https://akkoma.dev/AkkomaGang/file_ex.git", "cc7067c7d446c2526e9ecf91d40896b088851569", [ref: "cc7067c7d446c2526e9ecf91d40896b088851569"]},
+ "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
- "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
+ "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"},
"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,11 +62,11 @@
"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.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
- "joken": {:hex, :joken, "2.6.1", "2ca3d8d7f83bf7196296a3d9b2ecda421a404634bfc618159981a960020480a1", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ab26122c400b3d254ce7d86ed066d6afad27e70416df947cdcb01e13a7382e68"},
+ "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
- "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
+ "mail": {:hex, :mail, "0.4.2", "959229676ef11dfdfb1269ce4a611622cb70415a1d6f925d79e547848bafc14d", [:mix], [], "hexpm", "08e5b70c72b8d1605cb88ef2df2c7e41d002210a621503ea1c13f1a7916b6bd3"},
"majic": {:git, "https://akkoma.dev/AkkomaGang/majic.git", "80540b36939ec83f48e76c61e5000e0fd67706f0", [ref: "80540b36939ec83f48e76c61e5000e0fd67706f0"]},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
@@ -77,15 +79,16 @@
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
"mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"},
"mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"},
- "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
+ "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
+ "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"oban": {:hex, :oban, "2.17.12", "33fb0cbfb92b910d48dd91a908590fe3698bb85eacec8cd0d9bc6aa13dddd6d6", [: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", "7a647d6cd6bb300073db17faabce22d80ae135da3baf3180a064fa7c4fa046e3"},
- "open_api_spex": {:hex, :open_api_spex, "3.20.1", "ce5b3db013cd7337ab147f39fa2d4d627ddeeb4ff3fea34792f43d7e2e654605", [: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 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dc9c383949d0fc4b20b73103ac20af39dad638b3a15c0e6281853c2fc7cc3cc8"},
+ "open_api_spex": {:hex, :open_api_spex, "3.21.2", "6a704f3777761feeb5657340250d6d7332c545755116ca98f33d4b875777e1e5", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f42ae6ed668b895ebba3e02773cfb4b41050df26f803f2ef634c72a7687dc387"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [: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", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
- "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
@@ -94,14 +97,15 @@
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
- "plug_cowboy": {:hex, :plug_cowboy, "2.7.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_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [: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", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
+ "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
"pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
- "recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"},
+ "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
"remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
"search_parser": {:git, "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git", "08971a81e68686f9ac465cfb6661d51c5e4e1e7f", [ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"]},
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
@@ -111,20 +115,22 @@
"swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
- "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
+ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
+ "telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"},
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
- "tesla": {:hex, :tesla, "1.12.0", "170d64a3f566b9b614d60f53b206b2b91db8e29a48fed7553c40baf5b4c5f1fd", [: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", "2fd07d9e3fdb0e39dd2cb16064e28ce02808a26cf33a44d269ef3b1fc20fc914"},
+ "temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
+ "tesla": {:hex, :tesla, "1.13.0", "24a068a48d107080dd7c943a593997eee265977a38020eb2ab657cca78a12502", [: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]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7b8fc8f6b0640fa0d090af7889d12eb396460e044b6f8688a8e55e30406a2200"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
- "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
+ "tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
"ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
"vex": {:hex, :vex, "0.9.2", "fe061acc9e0907d983d46b51bf35d58176f0fe6eb7ba3b33c9336401bf42b6d1", [:mix], [], "hexpm", "76e709a9762e98c6b462dfce92e9b5dfbf712839227f2da8add6dd11549b12cb"},
"web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
- "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
+ "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [: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", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
}
diff --git a/priv/repo/migrations/20240625213637_create_signing_key_table.exs b/priv/repo/migrations/20240625213637_create_signing_key_table.exs
new file mode 100644
index 000000000..062edc66f
--- /dev/null
+++ b/priv/repo/migrations/20240625213637_create_signing_key_table.exs
@@ -0,0 +1,13 @@
+defmodule Pleroma.Repo.Migrations.CreateSigningKeyTable do
+ use Ecto.Migration
+
+ def change do
+ create table(:signing_keys, primary_key: false) do
+ add :user_id, references(:users, type: :uuid, on_delete: :delete_all)
+ add :key_id, :text, primary_key: true
+ add :public_key, :text
+ add :private_key, :text
+ timestamps()
+ end
+ end
+end
diff --git a/priv/repo/migrations/20240625220752_move_signing_keys.exs b/priv/repo/migrations/20240625220752_move_signing_keys.exs
new file mode 100644
index 000000000..9104b7c29
--- /dev/null
+++ b/priv/repo/migrations/20240625220752_move_signing_keys.exs
@@ -0,0 +1,37 @@
+defmodule Pleroma.Repo.Migrations.MoveSigningKeys do
+ use Ecto.Migration
+ alias Pleroma.User
+ alias Pleroma.Repo
+ import Ecto.Query
+
+ def up do
+ # we do not handle remote users here!
+ # because we want to store a key id -> user id mapping, and we don't
+ # currently store key ids for remote users...
+ query =
+ from(u in User)
+ |> where(local: true)
+
+ Repo.stream(query, timeout: :infinity)
+ |> Enum.each(fn
+ %User{id: user_id, keys: private_key, local: true, ap_id: ap_id} ->
+ IO.puts("Migrating user #{user_id}")
+ # we can precompute the public key here...
+ # we do use it on every user view which makes it a bit of a dos attack vector
+ # so we should probably cache it
+ {:ok, public_key} = User.SigningKey.private_pem_to_public_pem(private_key)
+
+ key = %User.SigningKey{
+ user_id: user_id,
+ public_key: public_key,
+ key_id: User.SigningKey.local_key_id(ap_id),
+ private_key: private_key
+ }
+
+ {:ok, _} = Repo.insert(key)
+ end)
+ end
+
+ # no need to rollback
+ def down, do: :ok
+end
diff --git a/priv/repo/migrations/20241126093029_add_signing_key_index.exs b/priv/repo/migrations/20241126093029_add_signing_key_index.exs
new file mode 100644
index 000000000..25df1bb7f
--- /dev/null
+++ b/priv/repo/migrations/20241126093029_add_signing_key_index.exs
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.AddSigningKeyIndex do
+ use Ecto.Migration
+
+ def change do
+ create_if_not_exists(index(:signing_keys, [:user_id], name: :signing_keys_user_id_index))
+ end
+end
diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs
index 3b09f656b..75c67efb4 100644
--- a/test/mix/tasks/pleroma/config_test.exs
+++ b/test/mix/tasks/pleroma/config_test.exs
@@ -51,7 +51,6 @@ test "error if file with custom settings doesn't exist" do
clear_config(:configurable_from_database, true)
end
- @tag capture_log: true
test "config migration refused when deprecated settings are found" do
clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"])
assert config_records() == []
diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs
index 20f113e6e..4f97a978a 100644
--- a/test/mix/tasks/pleroma/database_test.exs
+++ b/test/mix/tasks/pleroma/database_test.exs
@@ -353,7 +353,7 @@ test "with the --keep-threads option it keeps old threads with bookmarked posts"
test "We don't have unexpected tables which may contain objects that are referenced by activities" do
# We can delete orphaned activities. For that we look for the objects they reference in the 'objects', 'activities', and 'users' table.
- # If someone adds another table with objects (idk, maybe with separate relations, or collections or w/e), then we need to make sure we
+ # If someone adds another table with objects (idk, maybe with separate relations, or collections or w/e), then we need to make sure we
# add logic for that in the 'prune_objects' task so that we don't wrongly delete their corresponding activities.
# So when someone adds (or removes) a table, this test will fail.
# Either the table contains objects which can be referenced from the activities table
@@ -401,6 +401,7 @@ test "We don't have unexpected tables which may contain objects that are referen
["rich_media_card"],
["scheduled_activities"],
["schema_migrations"],
+ ["signing_keys"],
["thread_mutes"],
["user_follows_hashtag"],
["user_frontend_setting_profiles"],
diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs
index f5d2e2eef..049f27a92 100644
--- a/test/pleroma/emoji/pack_test.exs
+++ b/test/pleroma/emoji/pack_test.exs
@@ -64,7 +64,10 @@ test "returns error when zip file is bad", %{pack: pack} do
path: Path.absname("test/instance_static/emoji/test_pack/blank.png")
}
- assert Pack.add_file(pack, nil, nil, file) == {:error, :einval}
+ # this varies by erlang OTP
+ possible_error_codes = [:bad_eocd, :einval]
+ {:error, code} = Pack.add_file(pack, nil, nil, file)
+ assert Enum.member?(possible_error_codes, code)
end
test "returns pack when zip file is empty", %{pack: pack} do
diff --git a/test/pleroma/keys_test.exs b/test/pleroma/keys_test.exs
deleted file mode 100644
index 9a15bf06e..000000000
--- a/test/pleroma/keys_test.exs
+++ /dev/null
@@ -1,24 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.KeysTest do
- use Pleroma.DataCase, async: true
-
- alias Pleroma.Keys
-
- test "generates an RSA private key pem" do
- {:ok, key} = Keys.generate_rsa_pem()
-
- assert is_binary(key)
- assert Regex.match?(~r/RSA/, key)
- end
-
- test "returns a public and private key from a pem" do
- pem = File.read!("test/fixtures/private_key.pem")
- {:ok, private, public} = Keys.keys_from_pem(pem)
-
- assert elem(private, 0) == :RSAPrivateKey
- assert elem(public, 0) == :RSAPublicKey
- end
-end
diff --git a/test/pleroma/object/containment_test.exs b/test/pleroma/object/containment_test.exs
index f8f40a3ac..1a1d01473 100644
--- a/test/pleroma/object/containment_test.exs
+++ b/test/pleroma/object/containment_test.exs
@@ -9,7 +9,6 @@ defmodule Pleroma.Object.ContainmentTest do
alias Pleroma.User
import Pleroma.Factory
- import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -136,23 +135,17 @@ test "contain_id_to_fetch() allows matching IDs" do
)
end
- test "contain_id_to_fetch() allows display URLs" do
+ test "contain_id_to_fetch() allows fragments and normalises domain casing" do
data = %{
- "id" => "http://example.com/~alyssa/activities/1234.json",
- "url" => "http://example.com/@alyssa/status/1234"
+ "id" => "http://example.com/users/capybara",
+ "url" => "http://example.com/@capybara"
}
- :ok =
- Containment.contain_id_to_fetch(
- "http://example.com/@alyssa/status/1234",
- data
- )
-
- :ok =
- Containment.contain_id_to_fetch(
- "http://example.com/@alyssa/status/1234/",
- data
- )
+ assert :ok ==
+ Containment.contain_id_to_fetch(
+ "http://EXAMPLE.com/users/capybara#key",
+ data
+ )
end
test "users cannot be collided through fake direction spoofing attempts" do
@@ -164,10 +157,14 @@ test "users cannot be collided through fake direction spoofing attempts" do
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
})
- assert capture_log(fn ->
- {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
- end) =~
- "[error] Could not decode user at fetch https://n1u.moe/users/rye"
+ # Fetch from an attempted spoof id will suceed, but automatically retrieve
+ # the real data from the homeserver instead of naïvely using the spoof
+ {:ok, fetched_user} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
+
+ refute fetched_user.name == "evil rye"
+ refute fetched_user.raw_bio == "boooo!"
+ assert fetched_user.name == "♡ rye ♡"
+ assert fetched_user.nickname == "rye@niu.moe"
end
test "contain_origin_from_id() gracefully handles cases where no ID is present" do
diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs
index 12154cb05..6b87635eb 100644
--- a/test/pleroma/object/fetcher_test.exs
+++ b/test/pleroma/object/fetcher_test.exs
@@ -22,6 +22,7 @@ defp spoofed_object_with_ids(
|> Jason.decode!()
|> Map.put("id", id)
|> Map.put("actor", actor_id)
+ |> Map.put("attributedTo", actor_id)
|> Jason.encode!()
end
@@ -109,7 +110,7 @@ defp spoofed_object_with_ids(
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_media_redirect1")
}
- # Spoof: cross-domain redirect with final domain id
+ # Spoof: cross-domain redirect with final domain id, but original id actor
%{method: :get, url: "https://patch.cx/objects/spoof_media_redirect2"} ->
%Tesla.Env{
status: 200,
@@ -118,6 +119,19 @@ defp spoofed_object_with_ids(
body: spoofed_object_with_ids("https://media.patch.cx/objects/spoof_media_redirect2")
}
+ # No-Spoof: cross-domain redirect with id and actor from final domain
+ %{method: :get, url: "https://patch.cx/objects/spoof_media_redirect3"} ->
+ %Tesla.Env{
+ status: 200,
+ url: "https://media.patch.cx/objects/spoof_media_redirect3",
+ headers: [{"content-type", "application/activity+json"}],
+ body:
+ spoofed_object_with_ids(
+ "https://media.patch.cx/objects/spoof_media_redirect3",
+ "https://media.patch.cx/users/rin"
+ )
+ }
+
# No-Spoof: same domain redirect
%{method: :get, url: "https://patch.cx/objects/spoof_redirect"} ->
%Tesla.Env{
@@ -189,7 +203,6 @@ defp spoofed_object_with_ids(
:ok
end
- @tag capture_log: true
test "it works when fetching the OP actor errors out" do
# Here we simulate a case where the author of the OP can't be read
assert {:ok, _} =
@@ -252,7 +265,7 @@ 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, :id_mismatch} =
+ assert {:error, :not_found} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
)
@@ -264,19 +277,29 @@ test "it does not fetch a spoofed object with id different from URL" do
end
test "it does not fetch an object via cross-domain redirects (initial id)" do
- assert {:error, {:cross_domain_redirect, true}} =
+ assert {:error, {:containment, _}} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/objects/spoof_media_redirect1"
)
end
- test "it does not fetch an object via cross-domain redirects (final id)" do
- assert {:error, {:cross_domain_redirect, true}} =
+ test "it does not fetch an object via cross-domain redirect if the actor is from the original domain" do
+ assert {:error, {:containment, :error}} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/objects/spoof_media_redirect2"
)
end
+ test "it allows cross-domain redirects when id and author are from final domain" do
+ assert {:ok, %{"id" => id, "attributedTo" => author}} =
+ Fetcher.fetch_and_contain_remote_object_from_id(
+ "https://patch.cx/objects/spoof_media_redirect3"
+ )
+
+ assert URI.parse(id).host == "media.patch.cx"
+ assert URI.parse(author).host == "media.patch.cx"
+ end
+
test "it accepts same-domain redirects" do
assert {:ok, %{"id" => id} = _object} =
Fetcher.fetch_and_contain_remote_object_from_id(
@@ -755,7 +778,7 @@ test "should return ok if the content type is application/activity+json" do
assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
end
- test "should return ok if the content type is application/ld+json with a profile" do
+ test "should return ok if the content type is application/ld+json with the ActivityStream profile" do
Tesla.Mock.mock(fn
%{
method: :get,
@@ -775,6 +798,26 @@ test "should return ok if the content type is application/ld+json with a profile
assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
end
+ test "should return ok if the content type is application/ld+json with several profiles" do
+ Tesla.Mock.mock(fn
+ %{
+ method: :get,
+ url: "https://mastodon.social/2"
+ } ->
+ %Tesla.Env{
+ status: 200,
+ url: "https://mastodon.social/2",
+ headers: [
+ {"content-type",
+ "application/ld+json; profile=\"https://example.org/ns/superduperspec https://www.w3.org/ns/activitystreams\""}
+ ],
+ body: "{}"
+ }
+ end)
+
+ assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
+ end
+
test "should not return ok with other content types" do
Tesla.Mock.mock(fn
%{
diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs
index 768c78f21..86e2b138a 100644
--- a/test/pleroma/signature_test.exs
+++ b/test/pleroma/signature_test.exs
@@ -6,7 +6,6 @@ defmodule Pleroma.SignatureTest do
use Pleroma.DataCase, async: false
@moduletag :mocked
- import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
import Mock
@@ -35,25 +34,23 @@ defp make_fake_conn(key_id),
do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}}
describe "fetch_public_key/1" do
- test "it returns key" do
+ test "it returns the key" do
expected_result = {:ok, @rsa_public_key}
- user = insert(:user, public_key: @public_key)
+ user =
+ insert(:user)
+ |> with_signing_key(public_key: @public_key)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
end
- test "it returns error when not found user" do
- assert capture_log(fn ->
- assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) ==
- {:error, :error}
- end) =~ "[error] Could not decode user"
- end
-
test "it returns error if public key is nil" do
- user = insert(:user, public_key: nil)
+ # this actually needs the URL to be valid
+ user = insert(:user)
+ key_id = user.ap_id <> "#main-key"
+ Tesla.Mock.mock(fn %{url: ^key_id} -> {:ok, %{status: 404}} end)
- assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error}
+ assert {:error, _} = Signature.fetch_public_key(make_fake_conn(user.ap_id))
end
end
@@ -63,12 +60,6 @@ test "it returns key" do
assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key}
end
-
- test "it returns error when not found user" do
- assert capture_log(fn ->
- {:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id"))
- end) =~ "[error] Could not decode user"
- end
end
defp split_signature(sig) do
@@ -104,9 +95,9 @@ defp assert_part_equal(part_a, part_b) do
test "it returns signature headers" do
user =
insert(:user, %{
- ap_id: "https://mastodon.social/users/lambadalambda",
- keys: @private_key
+ ap_id: "https://mastodon.social/users/lambadalambda"
})
+ |> with_signing_key(private_key: @private_key)
headers = %{
host: "test.test",
@@ -121,50 +112,15 @@ test "it returns signature headers" do
"keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\""
)
end
-
- test "it returns error" do
- user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""})
-
- assert Signature.sign(
- user,
- %{host: "test.test", "content-length": "100"}
- ) == {:error, []}
- end
end
describe "key_id_to_actor_id/1" do
- test "it properly deduces the actor id for misskey" do
- assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
- {:ok, "https://example.com/users/1234"}
- end
+ test "it reverses the key id to actor id" do
+ user =
+ insert(:user)
+ |> with_signing_key()
- test "it properly deduces the actor id for mastodon and pleroma" do
- assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") ==
- {:ok, "https://example.com/users/1234"}
- end
-
- test "it deduces the actor id for gotoSocial" do
- assert Signature.key_id_to_actor_id("https://example.com/users/1234/main-key") ==
- {:ok, "https://example.com/users/1234"}
- end
-
- test "it deduces the actor ID for streams" do
- assert Signature.key_id_to_actor_id("https://example.com/users/1234?operation=getkey") ==
- {:ok, "https://example.com/users/1234"}
- end
-
- test "it deduces the actor ID for bridgy" do
- assert Signature.key_id_to_actor_id("https://example.com/1234#key") ==
- {:ok, "https://example.com/1234"}
- end
-
- test "it calls webfinger for 'acct:' accounts" do
- with_mock(Pleroma.Web.WebFinger,
- finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end
- ) do
- assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") ==
- {:ok, "https://gensokyo.2hu/users/raymoo"}
- end
+ assert Signature.key_id_to_actor_id(user.signing_key.key_id) == {:ok, user.ap_id}
end
end
diff --git a/test/pleroma/upload/filter/exiftool/strip_location_test.exs b/test/pleroma/upload/filter/exiftool/strip_location_test.exs
index 2e017cd7e..1f798556b 100644
--- a/test/pleroma/upload/filter/exiftool/strip_location_test.exs
+++ b/test/pleroma/upload/filter/exiftool/strip_location_test.exs
@@ -115,6 +115,15 @@ test "verify webp files are skipped" do
assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop}
end
+ test "verify bmp files are skipped" do
+ upload = %Pleroma.Upload{
+ name: "sample.bmp",
+ content_type: "image/bmp"
+ }
+
+ assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop}
+ end
+
test "verify svg files are skipped" do
upload = %Pleroma.Upload{
name: "sample.svg",
diff --git a/test/pleroma/user_search_test.exs b/test/pleroma/user_search_test.exs
index 2af19b6de..b499629c8 100644
--- a/test/pleroma/user_search_test.exs
+++ b/test/pleroma/user_search_test.exs
@@ -259,7 +259,7 @@ test "works with URIs" do
|> Map.put(:multi_factor_authentication_settings, nil)
|> Map.put(:notification_settings, nil)
- assert user == expected
+ assert_user_match(user, expected)
end
test "excludes a blocked users from search result" do
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index de71f4b95..ac886aaf9 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -639,11 +639,12 @@ test "it sets the password_hash, ap_id, private key and followers collection add
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
-
assert is_binary(changeset.changes[:password_hash])
- assert is_binary(changeset.changes[:keys])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
- assert is_binary(changeset.changes[:keys])
+ assert changeset.changes[:signing_key]
+ assert changeset.changes[:signing_key].valid?
+ assert is_binary(changeset.changes[:signing_key].changes.private_key)
+ assert is_binary(changeset.changes[:signing_key].changes.public_key)
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end
@@ -814,7 +815,6 @@ test "gets an existing user by fully qualified nickname, case insensitive" do
assert user == fetched_user
end
- @tag capture_log: true
test "returns nil if no user could be fetched" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
assert fetched_user == "not found nonexistant@social.heldscal.la"
@@ -871,7 +871,6 @@ test "if nicknames clash, the old user gets a prefix with the old id to the nick
assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org"
end
- @tag capture_log: true
test "it returns the old user if stale, but unfetchable" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
@@ -967,6 +966,21 @@ test "it is invalid given a local user" do
refute cs.valid?
end
+
+ test "it truncates fields" do
+ clear_config([:instance, :max_remote_account_fields], 2)
+
+ fields = [
+ %{"name" => "One", "value" => "Uno"},
+ %{"name" => "Two", "value" => "Dos"},
+ %{"name" => "Three", "value" => "Tres"}
+ ]
+
+ cs = User.remote_user_changeset(@valid_remote |> Map.put(:fields, fields))
+
+ assert [%{"name" => "One", "value" => "Uno"}, %{"name" => "Two", "value" => "Dos"}] ==
+ Ecto.Changeset.get_field(cs, :fields)
+ end
end
describe "followers and friends" do
@@ -1149,6 +1163,18 @@ test "it blocks people" do
assert User.blocks?(user, blocked_user)
end
+ test "it blocks domains" do
+ user = insert(:user)
+ blocked_user = insert(:user)
+
+ refute User.blocks_domain?(user, blocked_user)
+
+ url = URI.parse(blocked_user.ap_id)
+ {:ok, user} = User.block_domain(user, url.host)
+
+ assert User.blocks_domain?(user, blocked_user)
+ end
+
test "it unblocks users" do
user = insert(:user)
blocked_user = insert(:user)
@@ -1159,6 +1185,17 @@ test "it unblocks users" do
refute User.blocks?(user, blocked_user)
end
+ test "it unblocks domains" do
+ user = insert(:user)
+ blocked_user = insert(:user)
+
+ url = URI.parse(blocked_user.ap_id)
+ {:ok, user} = User.block_domain(user, url.host)
+ {:ok, user} = User.unblock_domain(user, url.host)
+
+ refute User.blocks_domain?(user, blocked_user)
+ end
+
test "blocks tear down cyclical follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
@@ -1642,7 +1679,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
name: "qqqqqqq",
password_hash: "pdfk2$1b3n159001",
keys: "RSA begin buplic key",
- public_key: "--PRIVATE KEYE--",
avatar: %{"a" => "b"},
tags: ["qqqqq"],
banner: %{"a" => "b"},
@@ -1681,8 +1717,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
email: nil,
name: nil,
password_hash: nil,
- keys: "RSA begin buplic key",
- public_key: "--PRIVATE KEYE--",
avatar: %{},
tags: [],
last_refreshed_at: nil,
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 b325bcb9a..5b3697244 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -573,7 +573,6 @@ test "it inserts an incoming activity into the database", %{conn: conn} do
assert Activity.get_by_ap_id(data["id"])
end
- @tag capture_log: true
test "it inserts an incoming activity into the database" <>
"even if we can't fetch the user but have it in our db",
%{conn: conn} do
@@ -584,6 +583,7 @@ test "it inserts an incoming activity into the database" <>
local: false,
last_refreshed_at: nil
)
+ |> with_signing_key()
data =
File.read!("test/fixtures/mastodon-post-activity.json")
@@ -594,7 +594,7 @@ test "it inserts an incoming activity into the database" <>
conn =
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{user.ap_id}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
@@ -608,7 +608,10 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
sender_url = data["actor"]
- sender = insert(:user, ap_id: data["actor"])
+
+ sender =
+ insert(:user, ap_id: data["actor"])
+ |> with_signing_key()
Instances.set_consistently_unreachable(sender_url)
refute Instances.reachable?(sender_url)
@@ -616,7 +619,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do
conn =
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
@@ -641,7 +644,7 @@ test "accept follow activity", %{conn: conn} do
assert "ok" ==
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}#main-key\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", accept)
|> json_response(200)
@@ -678,6 +681,7 @@ test "accepts Add/Remove activities", %{conn: conn} do
|> String.replace("{{nickname}}", "lain")
actor = "https://example.com/users/lain"
+ key_id = "#{actor}/main-key"
insert(:user,
ap_id: actor,
@@ -705,6 +709,16 @@ test "accepts Add/Remove activities", %{conn: conn} do
headers: [{"content-type", "application/activity+json"}]
}
+ %{
+ method: :get,
+ url: ^key_id
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: user,
+ headers: [{"content-type", "application/activity+json"}]
+ }
+
%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{
status: 200,
@@ -778,12 +792,14 @@ test "mastodon pin/unpin", %{conn: conn} do
|> String.replace("{{nickname}}", "lain")
actor = "https://example.com/users/lain"
+ key_id = "#{actor}/main-key"
sender =
insert(:user,
ap_id: actor,
featured_address: "https://example.com/users/lain/collections/featured"
)
+ |> with_signing_key()
Tesla.Mock.mock(fn
%{
@@ -806,6 +822,16 @@ test "mastodon pin/unpin", %{conn: conn} do
headers: [{"content-type", "application/activity+json"}]
}
+ %{
+ method: :get,
+ url: ^key_id
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: user,
+ headers: [{"content-type", "application/activity+json"}]
+ }
+
%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{
status: 200,
@@ -839,7 +865,7 @@ test "mastodon pin/unpin", %{conn: conn} do
assert "ok" ==
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
@@ -901,7 +927,9 @@ test "it inserts an incoming activity into the database", %{conn: conn, data: da
end
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
- user = insert(:user)
+ user =
+ insert(:user)
+ |> with_signing_key()
data =
data
@@ -946,7 +974,9 @@ test "it accepts messages with cc as string instead of array", %{conn: conn, dat
end
test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
- user = insert(:user)
+ user =
+ insert(:user)
+ |> with_signing_key()
data =
data
@@ -973,7 +1003,10 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "hey"})
- announcer = insert(:user, local: false)
+
+ announcer =
+ insert(:user, local: false)
+ |> with_signing_key()
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
@@ -988,7 +1021,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do
conn =
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{announcer.ap_id}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{announcer.signing_key.key_id}\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -1003,7 +1036,10 @@ test "it accepts messages from actors that are followed by the user", %{
data: data
} do
recipient = insert(:user)
- actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
+
+ actor =
+ insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
+ |> with_signing_key()
{:ok, recipient, actor} = User.follow(recipient, actor)
@@ -1019,7 +1055,7 @@ test "it accepts messages from actors that are followed by the user", %{
conn =
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
@@ -1056,7 +1092,10 @@ test "it returns a note activity in a collection", %{conn: conn} do
end
test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
- user = insert(:user)
+ user =
+ insert(:user, ap_id: data["actor"])
+ |> with_signing_key()
+
data = Map.put(data, "bcc", [user.ap_id])
sender_host = URI.parse(data["actor"]).host
@@ -1066,7 +1105,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da
conn =
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -1074,9 +1113,9 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da
assert Instances.reachable?(sender_host)
end
- @tag capture_log: true
test "it removes all follower collections but actor's", %{conn: conn} do
[actor, recipient] = insert_pair(:user)
+ actor = with_signing_key(actor)
to = [
recipient.ap_id,
@@ -1105,7 +1144,7 @@ test "it removes all follower collections but actor's", %{conn: conn} do
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200)
@@ -1138,10 +1177,13 @@ test "it requires authentication", %{conn: conn} do
assert json_response(ret_conn, 200)
end
- @tag capture_log: true
test "forwarded report", %{conn: conn} do
admin = insert(:user, is_admin: true)
- actor = insert(:user, local: false)
+
+ actor =
+ insert(:user, local: false)
+ |> with_signing_key()
+
remote_domain = URI.parse(actor.ap_id).host
reported_user = insert(:user)
@@ -1198,7 +1240,7 @@ test "forwarded report", %{conn: conn} do
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)
@@ -1215,7 +1257,6 @@ test "forwarded report", %{conn: conn} do
)
end
- @tag capture_log: true
test "forwarded report from mastodon", %{conn: conn} do
admin = insert(:user, is_admin: true)
actor = insert(:user, local: false)
@@ -1232,12 +1273,22 @@ test "forwarded report from mastodon", %{conn: conn} do
|> File.read!()
|> String.replace("{{DOMAIN}}", remote_domain)
- Tesla.Mock.mock(fn %{url: ^remote_actor} ->
- %Tesla.Env{
- status: 200,
- body: mock_json_body,
- headers: [{"content-type", "application/activity+json"}]
- }
+ key_url = "#{remote_actor}#main-key"
+
+ Tesla.Mock.mock(fn
+ %{url: ^remote_actor} ->
+ %Tesla.Env{
+ status: 200,
+ body: mock_json_body,
+ headers: [{"content-type", "application/activity+json"}]
+ }
+
+ %{url: ^key_url} ->
+ %Tesla.Env{
+ status: 200,
+ body: mock_json_body,
+ headers: [{"content-type", "application/activity+json"}]
+ }
end)
data = %{
@@ -1254,7 +1305,7 @@ test "forwarded report from mastodon", %{conn: conn} do
conn
|> assign(:valid_signature, true)
- |> put_req_header("signature", "keyId=\"#{remote_actor}/main-key\"")
+ |> put_req_header("signature", "keyId=\"#{remote_actor}#main-key\"")
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)
diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs
index 87930b7b1..eeec59cfb 100644
--- a/test/pleroma/web/activity_pub/publisher_test.exs
+++ b/test/pleroma/web/activity_pub/publisher_test.exs
@@ -140,7 +140,9 @@ test "publish to url with with different ports" do
{:ok, %Tesla.Env{status: 200, body: "port 80"}}
end)
- actor = insert(:user)
+ actor =
+ insert(:user)
+ |> with_signing_key()
assert {:ok, %{body: "port 42"}} =
Publisher.publish_one(%{
@@ -165,7 +167,10 @@ test "publish to url with with different ports" do
Instances,
[:passthrough],
[] do
- actor = insert(:user)
+ actor =
+ insert(:user)
+ |> with_signing_key()
+
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
@@ -176,7 +181,10 @@ test "publish to url with with different ports" do
Instances,
[:passthrough],
[] do
- actor = insert(:user)
+ actor =
+ insert(:user)
+ |> with_signing_key()
+
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
@@ -195,7 +203,10 @@ test "publish to url with with different ports" do
Instances,
[:passthrough],
[] do
- actor = insert(:user)
+ actor =
+ insert(:user)
+ |> with_signing_key()
+
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
@@ -214,7 +225,10 @@ test "publish to url with with different ports" do
Instances,
[:passthrough],
[] do
- actor = insert(:user)
+ actor =
+ insert(:user)
+ |> with_signing_key()
+
inbox = "http://404.site/users/nick1/inbox"
assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
@@ -226,7 +240,10 @@ test "publish to url with with different ports" do
Instances,
[:passthrough],
[] do
- actor = insert(:user)
+ actor =
+ insert(:user)
+ |> with_signing_key()
+
inbox = "http://connrefused.site/users/nick1/inbox"
assert capture_log(fn ->
@@ -241,7 +258,10 @@ test "publish to url with with different ports" do
Instances,
[:passthrough],
[] do
- actor = insert(:user)
+ actor =
+ insert(:user)
+ |> with_signing_key()
+
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
@@ -253,7 +273,10 @@ test "publish to url with with different ports" do
Instances,
[:passthrough],
[] do
- actor = insert(:user)
+ actor =
+ insert(:user)
+ |> with_signing_key()
+
inbox = "http://connrefused.site/users/nick1/inbox"
assert capture_log(fn ->
@@ -294,7 +317,9 @@ test "publish to url with with different ports" do
ap_enabled: true
})
- actor = insert(:user, follower_address: follower.ap_id)
+ actor =
+ insert(:user, follower_address: follower.ap_id)
+ |> with_signing_key()
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
{:ok, _another_follower, actor} = Pleroma.User.follow(another_follower, actor)
@@ -365,7 +390,9 @@ test "publish to url with with different ports" do
ap_enabled: true
})
- actor = insert(:user, follower_address: follower.ap_id)
+ actor =
+ insert(:user, follower_address: follower.ap_id)
+ |> with_signing_key()
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
actor = refresh_record(actor)
diff --git a/test/pleroma/web/activity_pub/relay_test.exs b/test/pleroma/web/activity_pub/relay_test.exs
index 920aef9da..99cc2071e 100644
--- a/test/pleroma/web/activity_pub/relay_test.exs
+++ b/test/pleroma/web/activity_pub/relay_test.exs
@@ -114,7 +114,6 @@ test "returns error when activity not `Create` type" do
assert Relay.publish(activity) == {:error, "Not implemented"}
end
- @tag capture_log: true
test "returns error when activity not public" do
activity = insert(:direct_note_activity)
assert Relay.publish(activity) == {:error, false}
diff --git a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs
index 524acddaf..88f42cada 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs
@@ -83,7 +83,6 @@ test "it works for incoming announces, fetching the announced object" do
assert(Activity.get_create_by_object_ap_id(data["object"]))
end
- @tag capture_log: true
test "it works for incoming announces with an existing activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
@@ -136,7 +135,6 @@ test "it works for incoming announces with an inlined activity" do
assert object.data["content"] == "this is a private toot"
end
- @tag capture_log: true
test "it rejects incoming announces with an inlined activity from another origin" do
Tesla.Mock.mock(fn
%{method: :get} -> %Tesla.Env{status: 404, body: ""}
diff --git a/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs
index b7160bf58..e5d6f940a 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs
@@ -86,7 +86,6 @@ test "it fails for incoming deletes with spoofed origin" do
assert match?({:error, _}, Transmogrifier.handle_incoming(data))
end
- @tag capture_log: true
test "it works for incoming user deletes" do
%{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
index 16ee31483..92a096c2d 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
@@ -57,7 +57,6 @@ test "it ignores an incoming notice if we already have it" do
assert activity == returned_activity
end
- @tag capture_log: true
test "it fetches reply-to activities if we don't have them" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
@@ -537,7 +536,6 @@ test "returns object with inReplyTo when denied incoming reply", %{data: data} d
assert modified_object["inReplyTo"] == []
end
- @tag capture_log: true
test "returns modified object when allowed incoming reply", %{data: data} do
object_with_reply =
Map.put(
@@ -700,7 +698,7 @@ test "take_emoji_tags/1" do
assert Transmogrifier.take_emoji_tags(user) == [
%{
"icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"},
- "id" => "https://example.org/firefox.png",
+ "id" => nil,
"name" => ":firefox:",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z"
diff --git a/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs
index b1a064772..35a5fe03d 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs
@@ -119,8 +119,8 @@ test "it works with custom profile fields" do
user = User.get_cached_by_ap_id(user.ap_id)
assert user.fields == [
- %{"name" => "foo", "value" => "updated"},
- %{"name" => "foo1", "value" => "updated"}
+ %{"name" => "foo", "value" => "bar"},
+ %{"name" => "foo11", "value" => "bar11"}
]
update_data =
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index dd7977593..1be69317c 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -561,7 +561,6 @@ test "returns nil when cannot normalize object" do
end) =~ ":valid_uri_scheme"
end
- @tag capture_log: true
test "returns {:ok, %Object{}} for success case" do
assert {:ok, %Object{}} =
Transmogrifier.get_obj_helper(
diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs
index abe91cdea..7e251e510 100644
--- a/test/pleroma/web/activity_pub/views/user_view_test.exs
+++ b/test/pleroma/web/activity_pub/views/user_view_test.exs
@@ -11,7 +11,9 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
alias Pleroma.Web.CommonAPI
test "Renders a user, including the public key" do
- user = insert(:user)
+ user =
+ insert(:user)
+ |> with_signing_key()
result = UserView.render("user.json", %{user: user})
@@ -37,13 +39,15 @@ test "Renders profile fields" do
end
test "Renders with emoji tags" do
- user = insert(:user, emoji: %{"bib" => "/test"})
+ user =
+ insert(:user, emoji: %{"bib" => "/test"})
+ |> with_signing_key()
assert %{
"tag" => [
%{
"icon" => %{"type" => "Image", "url" => "/test"},
- "id" => "/test",
+ "id" => nil,
"name" => ":bib:",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z"
@@ -74,13 +78,18 @@ test "Does not add an avatar image if the user hasn't set one" do
end
test "renders an invisible user with the invisible property set to true" do
- user = insert(:user, invisible: true)
+ user =
+ insert(:user, invisible: true)
+ |> with_signing_key()
assert %{"invisible" => true} = UserView.render("service.json", %{user: user})
end
test "service has a few essential fields" do
- user = insert(:user)
+ user =
+ insert(:user)
+ |> with_signing_key()
+
result = UserView.render("service.json", %{user: user})
assert result["id"]
assert result["type"] == "Application"
@@ -120,7 +129,9 @@ test "remote users have an empty endpoints structure" do
end
test "instance users do not expose oAuth endpoints" do
- user = insert(:user, nickname: nil, local: true)
+ user =
+ insert(:user, nickname: nil, local: true)
+ |> with_signing_key()
result = UserView.render("user.json", %{user: user})
diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs
index 6a0d25058..64364cfb3 100644
--- a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs
@@ -79,7 +79,6 @@ test "search", %{conn: conn} do
assert status["id"] == to_string(activity.id)
end
- @tag capture_log: true
test "constructs hashtags from search query", %{conn: conn} do
results =
conn
diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs
index 3e7b6730c..ccaf17fe4 100644
--- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs
@@ -152,7 +152,6 @@ test "filtering", %{conn: conn, user: user} do
end
describe "public" do
- @tag capture_log: true
test "the public timeline", %{conn: conn} do
user = insert(:user)
@@ -810,7 +809,6 @@ test "filtering", %{user: user, conn: conn} do
describe "hashtag" do
setup do: oauth_access(["n/a"])
- @tag capture_log: true
test "hashtag timeline", %{conn: conn} do
following = insert(:user)
diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
index 224b26cb9..91d95f229 100644
--- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
@@ -43,7 +43,7 @@ test "renders a poll" do
%{title: "why are you even asking?", votes_count: 0}
],
votes_count: 0,
- voters_count: 0
+ voters_count: nil
}
result = PollView.render("show.json", %{object: object})
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index 7db3e3e61..6421df132 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -33,6 +33,10 @@ test "has an emoji reaction list" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
+ domain_blocked_user = insert(:user, %{ap_id: "https://blocked.com/@blocked"})
+
+ {:ok, user} = User.block_domain(user, "blocked.com")
+
{:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
@@ -40,6 +44,8 @@ test "has an emoji reaction list" do
{:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:")
+ # this should not show up when the user is viewing the status
+ {:ok, _} = CommonAPI.react_with_emoji(activity.id, domain_blocked_user, "😈")
activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity)
@@ -55,7 +61,8 @@ test "has an emoji reaction list" do
url: "http://localhost:4001/emoji/dino walking.gif",
account_ids: [other_user.id, user.id]
},
- %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
+ %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]},
+ %{name: "😈", count: 1, me: false, url: nil, account_ids: [domain_blocked_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: user)
@@ -73,6 +80,8 @@ test "has an emoji reaction list" do
},
%{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
]
+
+ refute Enum.any?(status[:pleroma][:emoji_reactions], fn reaction -> reaction[:name] == "😈" end)
end
test "works correctly with badly formatted emojis" do
diff --git a/test/pleroma/web/plugs/http_signature_plug_test.exs b/test/pleroma/web/plugs/http_signature_plug_test.exs
index 0a602424d..5236b519e 100644
--- a/test/pleroma/web/plugs/http_signature_plug_test.exs
+++ b/test/pleroma/web/plugs/http_signature_plug_test.exs
@@ -14,6 +14,15 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
import Phoenix.Controller, only: [put_format: 2]
import Mock
+ setup do
+ user =
+ :user
+ |> insert(%{ap_id: "http://mastodon.example.org/users/admin"})
+ |> with_signing_key(%{key_id: "http://mastodon.example.org/users/admin#main-key"})
+
+ {:ok, %{user: user}}
+ end
+
setup_with_mocks([
{HTTPSignatures, [],
[
@@ -46,15 +55,15 @@ defp submit_to_plug(host, method, path) do
|> HTTPSignaturePlug.call(%{})
end
- test "it call HTTPSignatures to check validity if the actor signed it" do
- params = %{"actor" => "http://mastodon.example.org/users/admin"}
+ test "it call HTTPSignatures to check validity if the actor signed it", %{user: user} do
+ params = %{"actor" => user.ap_id}
conn = build_conn(:get, "/doesntmattter", params)
conn =
conn
|> put_req_header(
"signature",
- "keyId=\"http://mastodon.example.org/users/admin#main-key"
+ "keyId=\"#{user.signing_key.key_id}\""
)
|> put_format("activity+json")
|> HTTPSignaturePlug.call(%{})
diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs
index 086a27885..5789cd756 100644
--- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs
+++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs
@@ -8,52 +8,63 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
import Tesla.Mock
import Plug.Conn
+ import Pleroma.Factory
import Pleroma.Tests.Helpers, only: [clear_config: 2]
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
+
+ user =
+ insert(:user)
+ |> with_signing_key()
+
+ {:ok, %{user: user}}
end
- defp set_signature(conn, key_id) do
+ defp set_signature(conn, ap_id) do
conn
- |> put_req_header("signature", "keyId=\"#{key_id}\"")
+ |> put_req_header("signature", "keyId=\"#{ap_id}#main-key\"")
|> assign(:valid_signature, true)
end
- test "it successfully maps a valid identity with a valid signature" do
+ test "it successfully maps a valid identity with a valid signature", %{user: user} do
conn =
build_conn(:get, "/doesntmattter")
- |> set_signature("http://mastodon.example.org/users/admin")
+ |> set_signature(user.ap_id)
|> MappedSignatureToIdentityPlug.call(%{})
refute is_nil(conn.assigns.user)
end
- test "it successfully maps a valid identity with a valid signature with payload" do
+ test "it successfully maps a valid identity with a valid signature with payload", %{user: user} do
conn =
- build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
- |> set_signature("http://mastodon.example.org/users/admin")
+ build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
+ |> set_signature(user.ap_id)
|> MappedSignatureToIdentityPlug.call(%{})
refute is_nil(conn.assigns.user)
end
- test "it considers a mapped identity to be invalid when it mismatches a payload" do
+ test "it considers a mapped identity to be invalid when it mismatches a payload", %{user: user} do
conn =
- build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+ build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|> set_signature("https://niu.moe/users/rye")
|> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns
end
- test "it considers a mapped identity to be invalid when the associated instance is blocked" do
+ test "it considers a mapped identity to be invalid when the associated instance is blocked", %{
+ user: user
+ } do
clear_config([:activitypub, :authorized_fetch_mode], true)
+ # extract domain from user.ap_id
+ url = URI.parse(user.ap_id)
+
clear_config([:mrf_simple, :reject], [
- {"mastodon.example.org", "anime is banned"}
+ {url.host, "anime is banned"}
])
on_exit(fn ->
@@ -62,18 +73,21 @@ test "it considers a mapped identity to be invalid when the associated instance
end)
conn =
- build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
- |> set_signature("http://mastodon.example.org/users/admin")
+ build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
+ |> set_signature(user.ap_id)
|> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns
end
- test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed" do
+ test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed",
+ %{user: user} do
clear_config([:activitypub, :authorized_fetch_mode], true)
+ url = URI.parse(user.ap_id)
+
clear_config([:mrf_simple, :accept], [
- {"mastodon.example.org", "anime is allowed"}
+ {url.host, "anime is allowed"}
])
on_exit(fn ->
@@ -82,15 +96,16 @@ test "allowlist federation: it considers a mapped identity to be valid when the
end)
conn =
- build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
- |> set_signature("http://mastodon.example.org/users/admin")
+ build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
+ |> set_signature(user.ap_id)
|> MappedSignatureToIdentityPlug.call(%{})
assert conn.assigns[:valid_signature]
refute is_nil(conn.assigns.user)
end
- test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed" do
+ test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed",
+ %{user: user} do
clear_config([:activitypub, :authorized_fetch_mode], true)
clear_config([:mrf_simple, :accept], [
@@ -103,8 +118,8 @@ test "allowlist federation: it considers a mapped identity to be invalid when th
end)
conn =
- build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
- |> set_signature("http://mastodon.example.org/users/admin")
+ build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
+ |> set_signature(user.ap_id)
|> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns
diff --git a/test/pleroma/web/push/impl_test.exs b/test/pleroma/web/push/impl_test.exs
index a40c4ff13..f9936095f 100644
--- a/test/pleroma/web/push/impl_test.exs
+++ b/test/pleroma/web/push/impl_test.exs
@@ -67,7 +67,6 @@ test "performs sending notifications" do
assert Impl.perform(notif) == {:ok, [:ok, :ok]}
end
- @tag capture_log: true
test "returns error if notif does not match " do
assert Impl.perform(%{}) == {:error, :unknown_type}
end
@@ -76,7 +75,6 @@ test "successful message sending" do
assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok
end
- @tag capture_log: true
test "fail message sending" do
assert Impl.push_message(
@message,
diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs
index 2af084090..4fc2a8ffa 100644
--- a/test/pleroma/web/web_finger_test.exs
+++ b/test/pleroma/web/web_finger_test.exs
@@ -190,7 +190,6 @@ test "prevents spoofing" do
end
end
- @tag capture_log: true
test "prevents forgeries" do
Tesla.Mock.mock(fn
%{url: "https://bad.com/.well-known/webfinger?resource=acct:meanie@bad.com"} ->
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 2a73a4ae6..54e5f91b7 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -47,7 +47,6 @@ def instance_factory(attrs \\ %{}) do
end
def user_factory(attrs \\ %{}) do
- pem = Enum.random(@rsa_keys)
# Argon2.hash_pwd_salt("test")
# it really eats CPU time, so we use a precomputed hash
password_hash =
@@ -64,8 +63,7 @@ def user_factory(attrs \\ %{}) do
last_refreshed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{},
multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
- ap_enabled: true,
- keys: pem
+ ap_enabled: true
}
urls =
@@ -97,6 +95,28 @@ def user_factory(attrs \\ %{}) do
|> merge_attributes(attrs)
end
+ def with_signing_key(%User{} = user, attrs \\ %{}) do
+ signing_key =
+ build(:signing_key, %{user: user, key_id: "#{user.ap_id}#main-key"})
+ |> merge_attributes(attrs)
+
+ insert(signing_key)
+ %{user | signing_key: signing_key}
+ end
+
+ def signing_key_factory(attrs \\ %{}) do
+ pem = Enum.random(@rsa_keys)
+ user = attrs[:user] || insert(:user)
+ {:ok, public_key} = Pleroma.User.SigningKey.private_pem_to_public_pem(pem)
+
+ %Pleroma.User.SigningKey{
+ user_id: user.id,
+ public_key: attrs[:public_key] || public_key,
+ private_key: attrs[:private_key] || pem,
+ key_id: attrs[:key_id]
+ }
+ end
+
def user_relationship_factory(attrs \\ %{}) do
source = attrs[:source] || insert(:user)
target = attrs[:target] || insert(:user)
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index 2dfff70a2..f0beae8ec 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -66,6 +66,8 @@ defmacro __using__(_opts) do
clear_config: 2
]
+ import Pleroma.Test.MatchingHelpers
+
def time_travel(entity, seconds) do
new_time = NaiveDateTime.add(entity.inserted_at, seconds)
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 6a01393e3..d14434333 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -263,7 +263,12 @@ def get("https://n1u.moe/users/rye", _, _, @activitypub_accept_headers) do
{:ok,
%Tesla.Env{
status: 200,
- body: File.read!("test/fixtures/tesla_mock/rye.json"),
+ body:
+ File.read!("test/fixtures/tesla_mock/rye.json")
+ |> Jason.decode!()
+ |> Map.put("name", "evil rye")
+ |> Map.put("bio", "boooo!")
+ |> Jason.encode!(),
headers: activitypub_object_headers()
}}
end
@@ -419,6 +424,15 @@ def get("http://mastodon.example.org/users/admin", _, _, _) do
}}
end
+ def get("http://mastodon.example.org/users/admin/main-key", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
def get(
"http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true",
_,
@@ -953,6 +967,15 @@ def get("https://mastodon.social/users/lambadalambda", _, _, _) do
}}
end
+ def get("https://mastodon.social/users/lambadalambda#main-key", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/lambadalambda.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
def get("https://mastodon.social/users/lambadalambda/collections/featured", _, _, _) do
{:ok,
%Tesla.Env{
@@ -1398,6 +1421,15 @@ def get("https://relay.mastodon.host/actor", _, _, _) do
}}
end
+ def get("https://relay.mastodon.host/actor#main-key", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/relay/relay.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
def get("http://localhost:4001/", _, "", [{"accept", "text/html"}]) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}}
end
diff --git a/test/support/matching_helpers.ex b/test/support/matching_helpers.ex
new file mode 100644
index 000000000..c8411b907
--- /dev/null
+++ b/test/support/matching_helpers.ex
@@ -0,0 +1,10 @@
+defmodule Pleroma.Test.MatchingHelpers do
+ import ExUnit.Assertions
+
+ @assoc_fields [
+ :signing_key
+ ]
+ def assert_user_match(actor1, actor2) do
+ assert Ecto.reset_fields(actor1, @assoc_fields) == Ecto.reset_fields(actor2, @assoc_fields)
+ end
+end
diff --git a/test/test_helper.exs b/test/test_helper.exs
index dafa45099..6dcb87ff6 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -3,7 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
-ExUnit.start(exclude: [:federated, :erratic] ++ os_exclude)
+
+ExUnit.start(
+ capture_log: true,
+ exclude: [:federated, :erratic] ++ os_exclude
+)
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)