Compare commits

..

No commits in common. "develop" and "develop" have entirely different histories.

67 changed files with 510 additions and 1102 deletions

View file

@ -87,5 +87,5 @@ steps:
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate
- mkdir -p test/tmp - mkdir -p test/tmp
- mix test --preload-modules --exclude erratic --exclude federated --exclude mocked || mix test --failed - mix test --preload-modules --exclude erratic --exclude federated --exclude mocked
- mix test --preload-modules --only mocked || mix test --failed - mix test --preload-modules --only mocked

View file

@ -8,7 +8,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## BREAKING ## BREAKING
- Minimum PostgreSQL version is raised to 12 - Minimum PostgreSQL version is raised to 12
- Swagger UI moved from `/akkoma/swaggerui/` to `/pleroma/swaggerui/`
## Added ## Added
- Implement [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) (federation documentation) - Implement [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) (federation documentation)
@ -18,11 +17,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Fixed ## Fixed
- Meilisearch: order of results returned from our REST API now actually matches how Meilisearch ranks results - 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 ## Changed
- Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated. - Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated.

View file

@ -51,8 +51,7 @@
hostname: System.get_env("DB_HOST") || "localhost", hostname: System.get_env("DB_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox, pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 50, pool_size: 50,
queue_target: 5000, queue_target: 5000
log: false
config :pleroma, :dangerzone, override_repo_pool_size: true config :pleroma, :dangerzone, override_repo_pool_size: true

View file

@ -11,4 +11,4 @@ echo "-- Running migrations..."
mix ecto.migrate mix ecto.migrate
echo "-- Starting!" echo "-- Starting!"
elixir --erl "+sbwt none +sbwtdcpu none +sbwtdio none" -S mix phx.server mix phx.server

View file

@ -338,7 +338,7 @@ config :pleroma, :frontends,
* `:primary` - The frontend that will be served at `/` * `:primary` - The frontend that will be served at `/`
* `:admin` - The frontend that will be served at `/pleroma/admin` * `:admin` - The frontend that will be served at `/pleroma/admin`
* `:swagger` - Config for developers to act as an API reference to be served at `/pleroma/swaggerui/` (trailing slash _needed_). Disabled by default. * `:swagger` - Config for developers to act as an API reference to be served at `/akkoma/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. * `:mastodon` - The mastodon-fe configuration. This shouldn't need to be changed. This is served at `/web` when installed.
### :static\_fe ### :static\_fe

View file

@ -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. 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 `/pleroma/swaggerui` You will now be able to view documentation at `/akkoma/swaggerui`

View file

@ -6,9 +6,7 @@ probably install frontends.
These are no longer bundled with the distribution and need an extra These are no longer bundled with the distribution and need an extra
command to install. command to install.
You **must** run frontend management tasks as the akkoma user, For most installations, the following will suffice:
the same way you downloaded the build or cloned the git repo before.
But otherwise, for most installations, the following will suffice:
=== "OTP" === "OTP"
```sh ```sh
@ -30,3 +28,4 @@ But otherwise, for most installations, the following will suffice:
``` ```
For more customised installations, refer to [Frontend Management](../../configuration/frontend_management) For more customised installations, refer to [Frontend Management](../../configuration/frontend_management)

View file

@ -19,9 +19,6 @@ Environment="MIX_ENV=prod"
; Don't listen epmd on 0.0.0.0 ; Don't listen epmd on 0.0.0.0
Environment="ERL_EPMD_ADDRESS=127.0.0.1" 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. ; Make sure that all paths fit your installation.
; Path to the home directory of the user running the Akkoma service. ; Path to the home directory of the user running the Akkoma service.
Environment="HOME=/var/lib/akkoma" Environment="HOME=/var/lib/akkoma"

View file

@ -1,43 +1,23 @@
#!/sbin/openrc-run #!/sbin/openrc-run
supervisor=supervise-daemon supervisor=supervise-daemon
no_new_privs="yes" 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" pidfile="/var/run/akkoma.pid"
directory=/opt/akkoma
healthcheck_delay=60
healthcheck_timer=30
no_new_privs="yes"
# Ask process first to terminate itself within 60s, otherwise kill it : ${akkoma_port:-4000}
retry="SIGTERM/60/SIGKILL/5"
# if you really want to use start-stop-daemon instead, # Needs OpenRC >= 0.42
# also put the following in the config: #respawn_max=0
# command_background=1 #respawn_delay=5
# 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 if yesno "${akkoma_console}"; then
command=elixir 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" 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"
@ -51,24 +31,13 @@ else
command_args="phx.server" command_args="phx.server"
fi fi
export MIX_ENV=prod
export ERL_EPMD_ADDRESS=127.0.0.1
depend() { depend() {
need nginx postgresql 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() { healthcheck() {
# put akkoma_health=YES in /etc/conf.d/akkoma if you want healthchecking # put akkoma_health=YES in /etc/conf.d/akkoma if you want healthchecking
# and make sure you have curl installed # and make sure you have curl installed

View file

@ -111,7 +111,7 @@ def run(["get-packs" | args]) do
{:ok, _} = {:ok, _} =
:zip.unzip(binary_archive, :zip.unzip(binary_archive,
cwd: to_charlist(pack_path), cwd: pack_path,
file_list: files_to_unzip file_list: files_to_unzip
) )

View file

@ -125,7 +125,7 @@ def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"}
{:ok, _emoji_files} = {:ok, _emoji_files} =
:zip.unzip( :zip.unzip(
to_charlist(file.path), to_charlist(file.path),
[{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, to_charlist(tmp_dir)}] [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
) )
{_, updated_pack} = {_, updated_pack} =

View file

@ -79,10 +79,6 @@ def unzip(zip, dest) do
new_file_path = Path.join(dest, path) new_file_path = Path.join(dest, path)
new_file_path
|> Path.dirname()
|> File.rm()
new_file_path new_file_path
|> Path.dirname() |> Path.dirname()
|> File.mkdir_p!() |> File.mkdir_p!()

46
lib/pleroma/keys.ex Normal file
View file

@ -0,0 +1,46 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Keys do
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
try do
_ = :public_key.generate_key({:rsa, 2048, 65_537})
def generate_rsa_pem do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
{:ok, pem}
end
rescue
_ ->
def generate_rsa_pem do
port = Port.open({:spawn, "openssl genrsa"}, [:binary])
{:ok, pem} =
receive do
{^port, {:data, pem}} -> {:ok, pem}
end
Port.close(port)
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
{:ok, pem}
else
:error
end
end
end
def keys_from_pem(pem) do
with [private_key_code] <- :public_key.pem_decode(pem),
private_key <- :public_key.pem_entry_decode(private_key_code),
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do
{:ok, private_key, {:RSAPublicKey, modulus, exponent}}
else
error -> {:error, error}
end
end
end

View file

@ -12,6 +12,8 @@ defmodule Pleroma.Object.Containment do
spoofing, therefore removal of object containment functions is NOT recommended. spoofing, therefore removal of object containment functions is NOT recommended.
""" """
alias Pleroma.Web.ActivityPub.Transmogrifier
def get_actor(%{"actor" => actor}) when is_binary(actor) do def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor actor
end end
@ -48,39 +50,16 @@ def get_object(_) do
defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
defp compare_uris(_id_uri, _other_uri), do: :error defp compare_uris(_id_uri, _other_uri), do: :error
defp uri_strip_slash(%URI{path: path} = uri) when is_binary(path), defp compare_uris_exact(uri, uri), do: :ok
do: %{uri | path: String.replace_suffix(path, "/", "")}
defp uri_strip_slash(uri), do: uri defp compare_uris_exact(%URI{} = id, %URI{} = other),
do: compare_uris_exact(URI.to_string(id), URI.to_string(other))
# domain names are case-insensitive per spec (other parts of URIs arent necessarily) defp compare_uris_exact(id_uri, other_uri)
defp uri_normalise_host(%URI{host: host} = uri) when is_binary(host), when is_binary(id_uri) and is_binary(other_uri) do
do: %{uri | host: String.downcase(host, :ascii)} norm_id = String.replace_suffix(id_uri, "/", "")
norm_other = String.replace_suffix(other_uri, "/", "")
defp uri_normalise_host(uri), do: uri if norm_id == norm_other, do: :ok, else: :error
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 end
@doc """ @doc """
@ -114,13 +93,21 @@ def contain_origin(id, %{"attributedTo" => actor} = params),
def contain_origin(_id, _data), do: :ok def contain_origin(_id, _data), do: :ok
@doc """ @doc """
Check whether the fetch URL (after redirects) is the Check whether the fetch URL (after redirects) exactly (sans tralining slash) matches either
same location the canonical ActivityPub id points to. the canonical ActivityPub id or the objects url field (for display URLs from *key and Mastodon)
Since this is meant to be used for fetches, anonymous or transient objects are not accepted here. 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}) when is_binary(id) do def contain_id_to_fetch(url, %{"id" => id} = data) when is_binary(id) do
compare_uri_identities(url, id) with {:id, :error} <- {:id, compare_uris_exact(id, url)},
# "url" can be a "Link" object and this is checked before full normalisation
display_url <- Transmogrifier.fix_url(data)["url"],
true <- display_url != nil do
compare_uris_exact(display_url, url)
else
{:id, :ok} -> :ok
_ -> :error
end
end end
def contain_id_to_fetch(_url, _data), do: :error def contain_id_to_fetch(_url, _data), do: :error

View file

@ -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)" @doc "Assumes object already is in our database and refetches from remote to update (e.g. for polls)"
def refetch_object(%Object{data: %{"id" => id}} = object) do def refetch_object(%Object{data: %{"id" => id}} = object) do
with {:local, false} <- {:local, Object.local?(object)}, with {:local, false} <- {:local, Object.local?(object)},
{:ok, new_data} <- fetch_and_contain_remote_object_from_id(id, true), {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
{:id, true} <- {:id, new_data["id"] == id}, {:id, true} <- {:id, new_data["id"] == id},
{:ok, object} <- reinject_object(object, new_data) do {:ok, object} <- reinject_object(object, new_data) do
{:ok, object} {:ok, object}
@ -253,17 +253,14 @@ defp maybe_date_fetch(headers, date) do
end end
end end
@doc """ @doc "Fetches arbitrary remote object and performs basic safety and authenticity checks"
Fetches arbitrary remote object and performs basic safety and authenticity checks. def fetch_and_contain_remote_object_from_id(id)
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}, is_ap_id), def fetch_and_contain_remote_object_from_id(%{"id" => id}),
do: fetch_and_contain_remote_object_from_id(id, is_ap_id) do: fetch_and_contain_remote_object_from_id(id)
def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
Logger.debug("Fetching object #{id} via AP [ap_id=#{is_ap_id}]") Logger.debug("Fetching object #{id} via AP")
with {:valid_uri_scheme, true} <- {:valid_uri_scheme, String.starts_with?(id, "http")}, with {:valid_uri_scheme, true} <- {:valid_uri_scheme, String.starts_with?(id, "http")},
%URI{} = uri <- URI.parse(id), %URI{} = uri <- URI.parse(id),
@ -273,31 +270,18 @@ def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do
{:mrf_accept_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri)}, {:mrf_accept_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri)},
{:local_fetch, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)}, {:local_fetch, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)},
{:ok, final_id, body} <- get_object(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, data} <- safe_json_decode(body),
{_, :ok} <- {:containment, Containment.contain_origin(final_id, data)}, {_, :ok} <- {:strict_id, Containment.contain_id_to_fetch(final_id, data)},
{_, _, :ok} <- {:strict_id, data["id"], Containment.contain_id_to_fetch(final_id, data)} do {_, :ok} <- {:containment, Containment.contain_origin(final_id, data)} do
unless Instances.reachable?(final_id) do unless Instances.reachable?(final_id) do
Instances.set_reachable(final_id) Instances.set_reachable(final_id)
end end
{:ok, data} {:ok, data}
else else
# E.g. Mastodon and *key serve the AP object directly under their display URLs without {:strict_id, _} = e ->
# redirecting to their canonical location first, thus ids will expectedly differ. log_fetch_error(id, e)
# Similarly keys, either use a fragment ID and are a subobjects or a distinct ID {:error, :id_mismatch}
# 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 -> {:mrf_reject_check, _} = e ->
log_fetch_error(id, e) log_fetch_error(id, e)
@ -317,7 +301,7 @@ def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do
{:containment, reason} -> {:containment, reason} ->
log_fetch_error(id, reason) log_fetch_error(id, reason)
{:error, {:containment, reason}} {:error, reason}
{:error, e} -> {:error, e} ->
{:error, e} {:error, e}
@ -327,13 +311,25 @@ def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do
end end
end end
def fetch_and_contain_remote_object_from_id(_id, _is_ap_id), def fetch_and_contain_remote_object_from_id(_id),
do: {:error, :invalid_id} do: {:error, :invalid_id}
defp check_crossdomain_redirect(final_host, original_url)
# HOPEFULLY TEMPORARY # HOPEFULLY TEMPORARY
# Basically none of our Tesla mocks in tests set the (supposed to # Basically none of our Tesla mocks in tests set the (supposed to
# exist for Tesla proper) url parameter for their responses # exist for Tesla proper) url parameter for their responses
# causing almost every fetch in test to fail otherwise # 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 if @mix_env == :test do
defp get_final_id(nil, initial_url), do: initial_url defp get_final_id(nil, initial_url), do: initial_url
defp get_final_id("", initial_url), do: initial_url defp get_final_id("", initial_url), do: initial_url
@ -359,6 +355,10 @@ def get_object(id) do
with {:ok, %{body: body, status: code, headers: headers, url: final_url}} with {:ok, %{body: body, status: code, headers: headers, url: final_url}}
when code in 200..299 <- when code in 200..299 <-
HTTP.Backoff.get(id, headers), 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, {_, content_type}} <-
{:has_content_type, List.keyfind(headers, "content-type", 0)}, {:has_content_type, List.keyfind(headers, "content-type", 0)},
{:parse_content_type, {:ok, "application", subtype, type_params}} <- {:parse_content_type, {:ok, "application", subtype, type_params}} <-
@ -369,12 +369,8 @@ def get_object(id) do
{"activity+json", _} -> {"activity+json", _} ->
{:ok, final_id, body} {:ok, final_id, body}
{"ld+json", %{"profile" => profiles}} -> {"ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
if "https://www.w3.org/ns/activitystreams" in String.split(profiles) do {:ok, final_id, body}
{:ok, final_id, body}
else
{:error, {:content_type, content_type}}
end
_ -> _ ->
{:error, {:content_type, content_type}} {:error, {:content_type, content_type}}

View file

@ -5,25 +5,47 @@
defmodule Pleroma.Signature do defmodule Pleroma.Signature do
@behaviour HTTPSignatures.Adapter @behaviour HTTPSignatures.Adapter
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Keys
alias Pleroma.User alias Pleroma.User
alias Pleroma.User.SigningKey alias Pleroma.Web.ActivityPub.ActivityPub
require Logger
@known_suffixes ["/publickey", "/main-key", "#key"]
def key_id_to_actor_id(key_id) do def key_id_to_actor_id(key_id) do
case SigningKey.key_id_to_ap_id(key_id) do uri =
nil -> key_id
# hm, we SHOULD have gotten this in the pipeline before we hit here! |> URI.parse()
Logger.error("Could not figure out who owns the key #{key_id}") |> Map.put(:fragment, nil)
{:error, :key_owner_not_found} |> Map.put(:query, nil)
|> remove_suffix(@known_suffixes)
key -> maybe_ap_id = URI.to_string(uri)
{:ok, key}
case ObjectValidators.ObjectID.cast(maybe_ap_id) do
{:ok, ap_id} ->
{:ok, ap_id}
_ ->
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
{:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
_ -> {:error, maybe_ap_id}
end
end end
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 def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), 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, actor_id} <- key_id_to_actor_id(kid),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key} {:ok, public_key}
@ -35,8 +57,8 @@ def fetch_public_key(conn) do
def refetch_public_key(conn) do def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), 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, 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} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key} {:ok, public_key}
else else
@ -45,9 +67,9 @@ def refetch_public_key(conn) do
end end
end end
def sign(%User{} = user, headers) do def sign(%User{keys: keys} = user, headers) do
with {:ok, private_key} <- SigningKey.private_key(user) do with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, SigningKey.local_key_id(user.ap_id), headers) HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end end
end end

View file

@ -18,7 +18,6 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripMetadata do
# Formats not compatible with exiftool at this time # 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/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{content_type: "image/svg+xml"}), do: {:ok, :noop}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do

View file

@ -25,6 +25,7 @@ defmodule Pleroma.User do
alias Pleroma.Hashtag alias Pleroma.Hashtag
alias Pleroma.User.HashtagFollow alias Pleroma.User.HashtagFollow
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA alias Pleroma.MFA
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -42,7 +43,6 @@ defmodule Pleroma.User do
alias Pleroma.Web.OAuth alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe alias Pleroma.Web.RelMe
alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.BackgroundWorker
alias Pleroma.User.SigningKey
use Pleroma.Web, :verified_routes use Pleroma.Web, :verified_routes
@ -101,6 +101,7 @@ defmodule Pleroma.User do
field(:password, :string, virtual: true) field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true) field(:password_confirmation, :string, virtual: true)
field(:keys, :string) field(:keys, :string)
field(:public_key, :string)
field(:ap_id, :string) field(:ap_id, :string)
field(:avatar, :map, default: %{}) field(:avatar, :map, default: %{})
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
@ -221,10 +222,6 @@ defmodule Pleroma.User do
on_replace: :delete 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() timestamps()
end end
@ -460,7 +457,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|> fix_follower_address() |> fix_follower_address()
struct struct
|> Repo.preload(:signing_key)
|> cast( |> cast(
params, params,
[ [
@ -470,6 +466,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:inbox, :inbox,
:shared_inbox, :shared_inbox,
:nickname, :nickname,
:public_key,
:avatar, :avatar,
:ap_enabled, :ap_enabled,
:banner, :banner,
@ -498,7 +495,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|> validate_required([:ap_id]) |> validate_required([:ap_id])
|> validate_required([:name], trim: false) |> validate_required([:name], trim: false)
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, required: false)
|> validate_format(:nickname, @email_regex) |> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit) |> validate_length(:name, max: name_limit)
@ -530,6 +526,7 @@ def update_changeset(struct, params \\ %{}) do
:name, :name,
:emoji, :emoji,
:avatar, :avatar,
:public_key,
:inbox, :inbox,
:shared_inbox, :shared_inbox,
:is_locked, :is_locked,
@ -573,7 +570,6 @@ def update_changeset(struct, params \\ %{}) do
:pleroma_settings_store, :pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)} &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
) )
|> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, requred: false)
|> validate_fields(false, struct) |> validate_fields(false, struct)
end end
@ -832,10 +828,8 @@ def put_following_and_follower_and_featured_address(changeset) do
end end
defp put_private_key(changeset) do defp put_private_key(changeset) do
ap_id = get_field(changeset, :ap_id) {:ok, pem} = Keys.generate_rsa_pem()
put_change(changeset, :keys, pem)
changeset
|> put_assoc(:signing_key, SigningKey.generate_local_keys(ap_id))
end end
defp autofollow_users(user) do defp autofollow_users(user) do
@ -1152,8 +1146,7 @@ def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
was_superuser_before_update = User.superuser?(user) was_superuser_before_update = User.superuser?(user)
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
user set_cache(user)
|> set_cache()
end end
|> maybe_remove_report_notifications(was_superuser_before_update) |> maybe_remove_report_notifications(was_superuser_before_update)
end end
@ -1631,12 +1624,8 @@ def blocks_user?(%User{} = user, %User{} = target) do
def blocks_user?(_, _), do: false def blocks_user?(_, _), do: false
def blocks_domain?(%User{} = user, %User{ap_id: ap_id}) do def blocks_domain?(%User{} = user, %User{} = target) do
blocks_domain?(user, ap_id) %{host: host} = URI.parse(target.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) Enum.member?(user.domain_blocks, host)
# TODO: functionality should probably be changed such that subdomains block as well, # TODO: functionality should probably be changed such that subdomains block as well,
# but as it stands, this just hecks up the relationships endpoint # but as it stands, this just hecks up the relationships endpoint
@ -2058,16 +2047,24 @@ defp create_service_actor(uri, nickname) do
|> set_cache() |> set_cache()
end end
defdelegate public_key(user), to: SigningKey def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
key =
public_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
end
def public_key(_), do: {:error, "key not found"}
def get_public_key_for_ap_id(ap_id) do def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- SigningKey.public_key(user) do {:ok, public_key} <- public_key(user) do
{:ok, public_key} {:ok, public_key}
else else
e -> _ -> :error
Logger.error("Could not get public key for #{ap_id}.\n#{inspect(e)}")
{:error, e}
end end
end end

View file

@ -1,262 +0,0 @@
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

View file

@ -1547,17 +1547,6 @@ defp normalize_attachment(%{} = attachment), do: [attachment]
defp normalize_attachment(attachment) when is_list(attachment), do: attachment defp normalize_attachment(attachment) when is_list(attachment), do: attachment
defp normalize_attachment(_), do: [] 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 defp object_to_user_data(data, additional) do
fields = fields =
data data
@ -1589,16 +1578,9 @@ defp object_to_user_data(data, additional) do
featured_address = data["featured"] featured_address = data["featured"]
{:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address) {:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address)
# first, check that the owner is correct public_key =
signing_key = if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
if data["id"] !== data["publicKey"]["owner"] do data["publicKey"]["publicKeyPem"]
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 end
shared_inbox = shared_inbox =
@ -1642,7 +1624,7 @@ defp object_to_user_data(data, additional) do
bio: data["summary"] || "", bio: data["summary"] || "",
actor_type: actor_type, actor_type: actor_type,
also_known_as: also_known_as, also_known_as: also_known_as,
signing_key: signing_key, public_key: public_key,
inbox: data["inbox"], inbox: data["inbox"],
shared_inbox: shared_inbox, shared_inbox: shared_inbox,
pinned_objects: pinned_objects, pinned_objects: pinned_objects,

View file

@ -60,26 +60,7 @@ defp relay_active?(conn, _) do
end end
end end
@doc """ def user(conn, %{"nickname" => nickname}) do
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 with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
@ -91,18 +72,6 @@ defp render_full_user(conn, %{"nickname" => nickname}) do
end end
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 def object(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path, with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id), %Object{} = object <- Object.get_cached_by_ap_id(ap_id),

View file

@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating @behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.Signature
require Pleroma.Constants require Pleroma.Constants
@ -22,7 +23,8 @@ def validate(object, meta)
def validate(%{"type" => type, "id" => _id} = data, meta) def validate(%{"type" => type, "id" => _id} = data, meta)
when type in Pleroma.Constants.actor_types() do when type in Pleroma.Constants.actor_types() do
with :ok <- validate_inbox(data), with :ok <- validate_pubkey(data),
:ok <- validate_inbox(data),
:ok <- contain_collection_origin(data) do :ok <- contain_collection_origin(data) do
{:ok, data, meta} {:ok, data, meta}
else else
@ -33,6 +35,33 @@ def validate(%{"type" => type, "id" => _id} = data, meta)
def validate(_, _), do: {:error, "Not a user object"} 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 defp validate_inbox(%{"id" => id, "inbox" => inbox}) do
case Containment.same_origin(id, inbox) do case Containment.same_origin(id, inbox) do
:ok -> :ok :ok -> :ok

View file

@ -951,7 +951,7 @@ defp build_emoji_tag({name, url}) do
"name" => ":" <> name <> ":", "name" => ":" <> name <> ":",
"type" => "Emoji", "type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z", "updated" => "1970-01-01T00:00:00Z",
"id" => nil "id" => url
} }
end end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.UserView do defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Keys
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@ -32,7 +33,9 @@ def render("endpoints.json", %{user: %User{local: true} = _user}) do
def render("endpoints.json", _), do: %{} def render("endpoints.json", _), do: %{}
def render("service.json", %{user: user}) do def render("service.json", %{user: user}) do
{:ok, public_key} = User.SigningKey.public_key_pem(user) {: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])
endpoints = render("endpoints.json", %{user: user}) endpoints = render("endpoints.json", %{user: user})
@ -49,7 +52,7 @@ def render("service.json", %{user: user}) do
"url" => user.ap_id, "url" => user.ap_id,
"manuallyApprovesFollowers" => false, "manuallyApprovesFollowers" => false,
"publicKey" => %{ "publicKey" => %{
"id" => User.SigningKey.local_key_id(user.ap_id), "id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id, "owner" => user.ap_id,
"publicKeyPem" => public_key "publicKeyPem" => public_key
}, },
@ -67,12 +70,9 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
def render("user.json", %{user: user}) do def render("user.json", %{user: user}) do
public_key = {:ok, _, public_key} = Keys.keys_from_pem(user.keys)
case User.SigningKey.public_key_pem(user) do public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
{:ok, public_key} -> public_key public_key = :public_key.pem_encode([public_key])
_ -> nil
end
user = User.sanitize_html(user) user = User.sanitize_html(user)
endpoints = render("endpoints.json", %{user: user}) endpoints = render("endpoints.json", %{user: user})
@ -97,7 +97,7 @@ def render("user.json", %{user: user}) do
"url" => user.ap_id, "url" => user.ap_id,
"manuallyApprovesFollowers" => user.is_locked, "manuallyApprovesFollowers" => user.is_locked,
"publicKey" => %{ "publicKey" => %{
"id" => User.SigningKey.local_key_id(user.ap_id), "id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id, "owner" => user.ap_id,
"publicKeyPem" => public_key "publicKeyPem" => public_key
}, },
@ -116,20 +116,6 @@ def render("user.json", %{user: user}) do
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end 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 def render("following.json", %{user: user, page: page} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
showing_count = showing_items || !user.hide_follows_count showing_count = showing_items || !user.hide_follows_count

View file

@ -32,9 +32,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
}, },
voters_count: %Schema{ voters_count: %Schema{
type: :integer, type: :integer,
nullable: true, description: "How many unique accounts have voted. Number."
description:
"How many unique accounts have voted for a multi-selection poll. Number, or null if single-selection poll."
}, },
voted: %Schema{ voted: %Schema{
type: :boolean, type: :boolean,

View file

@ -66,10 +66,10 @@ defmodule Pleroma.Web.Endpoint do
} }
) )
plug(Plug.Static.IndexHtml, at: "/pleroma/swaggerui/") plug(Plug.Static.IndexHtml, at: "/akkoma/swaggerui")
plug(Pleroma.Web.Plugs.FrontendStatic, plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/pleroma/swaggerui", at: "/akkoma/swaggerui",
frontend_type: :swagger, frontend_type: :swagger,
gzip: true, gzip: true,
if: &Pleroma.Web.Swagger.ui_enabled?/0, if: &Pleroma.Web.Swagger.ui_enabled?/0,

View file

@ -19,7 +19,7 @@ def render("show.json", %{object: object, multiple: multiple, options: options}
expired: expired, expired: expired,
multiple: multiple, multiple: multiple,
votes_count: votes_count, votes_count: votes_count,
voters_count: voters_count(multiple, object), voters_count: voters_count(object),
options: options, options: options,
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]) emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
} }
@ -68,19 +68,11 @@ defp options_and_votes_count(options) do
end) end)
end end
defp voters_count(false, _poll_data) do defp voters_count(%{data: %{"voters" => voters}}) when is_list(voters) 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) length(voters)
end end
defp voters_count(_, _), do: 0 defp voters_count(_), do: 0
defp voted_and_own_votes(%{object: object} = params, options) do defp voted_and_own_votes(%{object: object} = params, options) do
if params[:for] do if params[:for] do

View file

@ -52,14 +52,6 @@ defp filter_allowed_user_by_ap_id(ap_ids, excluded_ap_ids) do
end) end)
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 def filter_allowed_users(reactions, user, with_muted) do
exclude_ap_ids = exclude_ap_ids =
if is_nil(user) do if is_nil(user) do
@ -70,10 +62,7 @@ def filter_allowed_users(reactions, user, with_muted) do
end end
filter_emoji = fn emoji, users, url -> filter_emoji = fn emoji, users, url ->
users case filter_allowed_user_by_ap_id(users, exclude_ap_ids) do
|> filter_allowed_user_by_ap_id(exclude_ap_ids)
|> filter_allowed_users_by_domain(user)
|> case do
[] -> nil [] -> nil
users -> {emoji, users, url} users -> {emoji, users, url}
end end

View file

@ -1,32 +0,0 @@
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

View file

@ -139,17 +139,12 @@ defp maybe_require_signature(
defp maybe_require_signature(conn), do: conn defp maybe_require_signature(conn), do: conn
defp signature_host(conn) do defp signature_host(conn) do
with {:key_id, %{"keyId" => kid}} <- {:key_id, HTTPSignatures.signature_for_conn(conn)}, with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:actor_id, {:ok, actor_id}} <- {:actor_id, Signature.key_id_to_actor_id(kid)} do {:ok, actor_id} <- Signature.key_id_to_actor_id(kid) do
actor_id actor_id
else else
{:key_id, e} -> e ->
Logger.error("Failed to extract key_id from signature: #{inspect(e)}") {:error, e}
nil
{:actor_id, e} ->
Logger.error("Failed to extract actor_id from signature: #{inspect(e)}")
nil
end end
end end
end end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
alias Pleroma.Helpers.AuthHelper alias Pleroma.Helpers.AuthHelper
alias Pleroma.Signature
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
@ -32,7 +33,7 @@ def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = con
|> assign(:valid_signature, false) |> assign(:valid_signature, false)
# remove me once testsuite uses mapped capabilities instead of what we do now # remove me once testsuite uses mapped capabilities instead of what we do now
{:user, _} -> {:user, nil} ->
Logger.debug("Failed to map identity from signature (lookup failure)") Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
@ -67,8 +68,7 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
Logger.debug("Failed to map identity from signature (lookup failure)") Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
conn only_permit_user_routes(conn)
|> assign(:valid_signature, false)
_ -> _ ->
Logger.debug("Failed to map identity from signature (no payload actor mismatch)") Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
@ -82,34 +82,33 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
# no signature at all # no signature at all
def call(conn, _opts), do: conn def call(conn, _opts), do: conn
defp key_id_from_conn(conn) do defp only_permit_user_routes(%{path_info: ["users", _]} = conn) do
case HTTPSignatures.signature_for_conn(conn) do conn
%{"keyId" => key_id} when is_binary(key_id) -> |> assign(:limited_ap, true)
key_id 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
_ -> _ ->
nil nil
end end
end end
defp user_from_key_id(conn) do defp user_from_key_id(conn) do
with {:key_id, key_id} when is_binary(key_id) <- {:key_id, key_id_from_conn(conn)}, with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
{:mapped_ap_id, ap_id} when is_binary(ap_id) <- {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do
{: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 user
else else
{:key_id, nil} -> _ ->
Logger.debug("Failed to map identity from signature (no key ID)") nil
{: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
end end

View file

@ -144,14 +144,7 @@ defmodule Pleroma.Web.Router do
}) })
end 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 pipeline :http_signature do
plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
plug(Pleroma.Web.Plugs.HTTPSignaturePlug) plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug) plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug)
@ -752,7 +745,7 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web do scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled # 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) # Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_xml_json, :optional_http_signature, :static_fe]) pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones # 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) get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)

View file

@ -144,7 +144,7 @@ defp deps do
{:ex_aws, "~> 2.4"}, {:ex_aws, "~> 2.4"},
{:ex_aws_s3, "~> 2.4"}, {:ex_aws_s3, "~> 2.4"},
{:sweet_xml, "~> 0.7"}, {:sweet_xml, "~> 0.7"},
{:earmark, "1.4.46"}, {:earmark, "~> 1.4"},
{:bbcode_pleroma, "~> 0.2.0"}, {:bbcode_pleroma, "~> 0.2.0"},
{:argon2_elixir, "~> 3.1"}, {:argon2_elixir, "~> 3.1"},
{:cors_plug, "~> 3.0"}, {:cors_plug, "~> 3.0"},
@ -180,7 +180,7 @@ defp deps do
{:remote_ip, "~> 1.1.0"}, {:remote_ip, "~> 1.1.0"},
{:captcha, {:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "6630c42aaaab124e697b4e513190c89d8b64e410"}, ref: "90f6ce7672f70f56708792a98d98bd05176c9176"},
{:restarter, path: "./restarter"}, {:restarter, path: "./restarter"},
{:majic, {:majic,
git: "https://akkoma.dev/AkkomaGang/majic.git", git: "https://akkoma.dev/AkkomaGang/majic.git",
@ -200,7 +200,7 @@ defp deps do
## dev & test ## dev & test
{:ex_doc, "~> 0.30", only: :dev, runtime: false}, {:ex_doc, "~> 0.30", only: :dev, runtime: false},
{:ex_machina, "~> 2.8", only: :test}, {:ex_machina, "~> 2.7", only: :test},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.8", only: :test}, {:mock, "~> 0.3.8", only: :test},
{:excoveralls, "0.16.1", only: :test}, {:excoveralls, "0.16.1", only: :test},

View file

@ -7,51 +7,51 @@
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "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"}, "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"}, "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", "6630c42aaaab124e697b4e513190c89d8b64e410", [ref: "6630c42aaaab124e697b4e513190c89d8b64e410"]}, "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]},
"castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
"concurrent_limiter": {:git, "https://akkoma.dev/AkkomaGang/concurrent-limiter.git", "a9e0b3d64574bdba761f429bb4fba0cf687b3338", [ref: "a9e0b3d64574bdba761f429bb4fba0cf687b3338"]}, "concurrent_limiter": {:git, "https://akkoma.dev/AkkomaGang/concurrent-limiter.git", "a9e0b3d64574bdba761f429bb4fba0cf687b3338", [ref: "a9e0b3d64574bdba761f429bb4fba0cf687b3338"]},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "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"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy": {:hex, :cowboy, "2.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"}, "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"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
"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"}, "credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [: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", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "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"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
"dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"}, "eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"},
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, "ecto": {:hex, :ecto, "3.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_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.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_psql_extras": {:hex, :ecto_psql_extras, "0.8.0", "440719cd74f09b3f01c84455707a2c3972b725c513808e68eb6c5b0ab82bf523", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 0.18.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "f1512812dc196bcb932a96c82e55f69b543dc125e9d39f5e3631a9c4ec65ef12"},
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, "ecto_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"]}, "elasticsearch": {:git, "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", "6cd946f75f6ab9042521a009d1d32d29a90113ca", [ref: "main"]},
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
"elixir_xml_to_map": {:hex, :elixir_xml_to_map, "3.1.0", "4d6260486a8cce59e4bf3575fe2dd2a24766546ceeef9f93fcec6f7c62a2827a", [:mix], [{:erlsom, "~> 1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "8fe5f2e75f90bab07ee2161120c2dc038ebcae8135554f5582990f1c8c21f911"}, "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "3.1.0", "4d6260486a8cce59e4bf3575fe2dd2a24766546ceeef9f93fcec6f7c62a2827a", [:mix], [{:erlsom, "~> 1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "8fe5f2e75f90bab07ee2161120c2dc038ebcae8135554f5582990f1c8c21f911"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"}, "erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"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": {: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.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_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"},
"ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"}, "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_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"},
"ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
"ex_syslogger": {:hex, :ex_syslogger, "2.0.0", "de6de5c5472a9c4fdafb28fa6610e381ae79ebc17da6490b81d785d68bd124c9", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "a52b2fe71764e9e6ecd149ab66635812f68e39279cbeee27c52c0e35e8b8019e"}, "ex_syslogger": {:hex, :ex_syslogger, "2.0.0", "de6de5c5472a9c4fdafb28fa6610e381ae79ebc17da6490b81d785d68bd124c9", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "a52b2fe71764e9e6ecd149ab66635812f68e39279cbeee27c52c0e35e8b8019e"},
"excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"}, "excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"},
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
"fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"}, "fast_html": {:hex, :fast_html, "2.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"}, "fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
"file_ex": {:git, "https://akkoma.dev/AkkomaGang/file_ex.git", "cc7067c7d446c2526e9ecf91d40896b088851569", [ref: "cc7067c7d446c2526e9ecf91d40896b088851569"]}, "file_ex": {:git, "https://akkoma.dev/AkkomaGang/file_ex.git", "cc7067c7d446c2526e9ecf91d40896b088851569", [ref: "cc7067c7d446c2526e9ecf91d40896b088851569"]},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"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"}, "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"}, "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.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "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"}, "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"}, "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"},
@ -61,43 +61,42 @@
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "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"}, "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"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, "joken": {:hex, :joken, "2.6.1", "2ca3d8d7f83bf7196296a3d9b2ecda421a404634bfc618159981a960020480a1", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ab26122c400b3d254ce7d86ed066d6afad27e70416df947cdcb01e13a7382e68"},
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
"mail": {:hex, :mail, "0.4.2", "959229676ef11dfdfb1269ce4a611622cb70415a1d6f925d79e547848bafc14d", [:mix], [], "hexpm", "08e5b70c72b8d1605cb88ef2df2c7e41d002210a621503ea1c13f1a7916b6bd3"}, "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
"majic": {:git, "https://akkoma.dev/AkkomaGang/majic.git", "80540b36939ec83f48e76c61e5000e0fd67706f0", [ref: "80540b36939ec83f48e76c61e5000e0fd67706f0"]}, "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": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "b21ab7754024af096f2d14247574f55f0063295b", [ref: "b21ab7754024af096f2d14247574f55f0063295b"]}, "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "b21ab7754024af096f2d14247574f55f0063295b", [ref: "b21ab7754024af096f2d14247574f55f0063295b"]},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, "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"}, "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"}, "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"},
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "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_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "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"}, "oban": {:hex, :oban, "2.17.10", "c3e5bd739b5c3fdc38eba1d43ab270a8c6ca4463bb779b7705c69400b0d87678", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4afd027b8e2bc3c399b54318b4f46ee8c40251fb55a285cb4e38b5363f0ee7c4"},
"open_api_spex": {:hex, :open_api_spex, "3.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"}, "open_api_spex": {:hex, :open_api_spex, "3.19.1", "65ccb5d06e3d664d1eec7c5ea2af2289bd2f37897094a74d7219fb03fc2b5994", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "392895827ce2984a3459c91a484e70708132d8c2c6c5363972b4b91d6bbac3dd"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "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": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.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_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"},
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, "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_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_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"}, "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"},
"plug": {:hex, :plug, "1.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": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.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_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "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"}, "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"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
@ -105,7 +104,7 @@
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, "postgrex": {:hex, :postgrex, "0.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"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"},
"remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"}, "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"]}, "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"}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
@ -115,22 +114,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"}, "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"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"}, "table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"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": {: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": {: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_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"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]}, "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"}, "tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "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"}, "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.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"}, "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"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
"vex": {:hex, :vex, "0.9.2", "fe061acc9e0907d983d46b51bf35d58176f0fe6eb7ba3b33c9336401bf42b6d1", [:mix], [], "hexpm", "76e709a9762e98c6b462dfce92e9b5dfbf712839227f2da8add6dd11549b12cb"}, "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"}, "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": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"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"}, "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
} }

View file

@ -1,13 +0,0 @@
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

View file

@ -1,37 +0,0 @@
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

View file

@ -51,6 +51,7 @@ test "error if file with custom settings doesn't exist" do
clear_config(:configurable_from_database, true) clear_config(:configurable_from_database, true)
end end
@tag capture_log: true
test "config migration refused when deprecated settings are found" do test "config migration refused when deprecated settings are found" do
clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"]) clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"])
assert config_records() == [] assert config_records() == []

View file

@ -401,7 +401,6 @@ test "We don't have unexpected tables which may contain objects that are referen
["rich_media_card"], ["rich_media_card"],
["scheduled_activities"], ["scheduled_activities"],
["schema_migrations"], ["schema_migrations"],
["signing_keys"],
["thread_mutes"], ["thread_mutes"],
["user_follows_hashtag"], ["user_follows_hashtag"],
["user_frontend_setting_profiles"], ["user_frontend_setting_profiles"],

View file

@ -64,10 +64,7 @@ test "returns error when zip file is bad", %{pack: pack} do
path: Path.absname("test/instance_static/emoji/test_pack/blank.png") path: Path.absname("test/instance_static/emoji/test_pack/blank.png")
} }
# this varies by erlang OTP assert Pack.add_file(pack, nil, nil, file) == {:error, :einval}
possible_error_codes = [:bad_eocd, :einval]
{:error, code} = Pack.add_file(pack, nil, nil, file)
assert Enum.member?(possible_error_codes, code)
end end
test "returns pack when zip file is empty", %{pack: pack} do test "returns pack when zip file is empty", %{pack: pack} do

View file

@ -0,0 +1,24 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Object.ContainmentTest do
alias Pleroma.User alias Pleroma.User
import Pleroma.Factory import Pleroma.Factory
import ExUnit.CaptureLog
setup_all do setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@ -135,17 +136,23 @@ test "contain_id_to_fetch() allows matching IDs" do
) )
end end
test "contain_id_to_fetch() allows fragments and normalises domain casing" do test "contain_id_to_fetch() allows display URLs" do
data = %{ data = %{
"id" => "http://example.com/users/capybara", "id" => "http://example.com/~alyssa/activities/1234.json",
"url" => "http://example.com/@capybara" "url" => "http://example.com/@alyssa/status/1234"
} }
assert :ok == :ok =
Containment.contain_id_to_fetch( Containment.contain_id_to_fetch(
"http://EXAMPLE.com/users/capybara#key", "http://example.com/@alyssa/status/1234",
data data
) )
:ok =
Containment.contain_id_to_fetch(
"http://example.com/@alyssa/status/1234/",
data
)
end end
test "users cannot be collided through fake direction spoofing attempts" do test "users cannot be collided through fake direction spoofing attempts" do
@ -157,14 +164,10 @@ test "users cannot be collided through fake direction spoofing attempts" do
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
}) })
# Fetch from an attempted spoof id will suceed, but automatically retrieve assert capture_log(fn ->
# the real data from the homeserver instead of naïvely using the spoof {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
{:ok, fetched_user} = 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"
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 end
test "contain_origin_from_id() gracefully handles cases where no ID is present" do test "contain_origin_from_id() gracefully handles cases where no ID is present" do

View file

@ -22,7 +22,6 @@ defp spoofed_object_with_ids(
|> Jason.decode!() |> Jason.decode!()
|> Map.put("id", id) |> Map.put("id", id)
|> Map.put("actor", actor_id) |> Map.put("actor", actor_id)
|> Map.put("attributedTo", actor_id)
|> Jason.encode!() |> Jason.encode!()
end end
@ -110,7 +109,7 @@ defp spoofed_object_with_ids(
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_media_redirect1") body: spoofed_object_with_ids("https://patch.cx/objects/spoof_media_redirect1")
} }
# Spoof: cross-domain redirect with final domain id, but original id actor # Spoof: cross-domain redirect with final domain id
%{method: :get, url: "https://patch.cx/objects/spoof_media_redirect2"} -> %{method: :get, url: "https://patch.cx/objects/spoof_media_redirect2"} ->
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -119,19 +118,6 @@ defp spoofed_object_with_ids(
body: spoofed_object_with_ids("https://media.patch.cx/objects/spoof_media_redirect2") 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 # No-Spoof: same domain redirect
%{method: :get, url: "https://patch.cx/objects/spoof_redirect"} -> %{method: :get, url: "https://patch.cx/objects/spoof_redirect"} ->
%Tesla.Env{ %Tesla.Env{
@ -203,6 +189,7 @@ defp spoofed_object_with_ids(
:ok :ok
end end
@tag capture_log: true
test "it works when fetching the OP actor errors out" do 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 # Here we simulate a case where the author of the OP can't be read
assert {:ok, _} = assert {:ok, _} =
@ -265,7 +252,7 @@ test "it does not fetch a spoofed object with wrong content type" do
end end
test "it does not fetch a spoofed object with id different from URL" do test "it does not fetch a spoofed object with id different from URL" do
assert {:error, :not_found} = assert {:error, :id_mismatch} =
Fetcher.fetch_and_contain_remote_object_from_id( Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json" "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
) )
@ -277,29 +264,19 @@ test "it does not fetch a spoofed object with id different from URL" do
end end
test "it does not fetch an object via cross-domain redirects (initial id)" do test "it does not fetch an object via cross-domain redirects (initial id)" do
assert {:error, {:containment, _}} = assert {:error, {:cross_domain_redirect, true}} =
Fetcher.fetch_and_contain_remote_object_from_id( Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/objects/spoof_media_redirect1" "https://patch.cx/objects/spoof_media_redirect1"
) )
end end
test "it does not fetch an object via cross-domain redirect if the actor is from the original domain" do test "it does not fetch an object via cross-domain redirects (final id)" do
assert {:error, {:containment, :error}} = assert {:error, {:cross_domain_redirect, true}} =
Fetcher.fetch_and_contain_remote_object_from_id( Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/objects/spoof_media_redirect2" "https://patch.cx/objects/spoof_media_redirect2"
) )
end 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 test "it accepts same-domain redirects" do
assert {:ok, %{"id" => id} = _object} = assert {:ok, %{"id" => id} = _object} =
Fetcher.fetch_and_contain_remote_object_from_id( Fetcher.fetch_and_contain_remote_object_from_id(
@ -778,7 +755,7 @@ test "should return ok if the content type is application/activity+json" do
assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2") assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
end end
test "should return ok if the content type is application/ld+json with the ActivityStream profile" do test "should return ok if the content type is application/ld+json with a profile" do
Tesla.Mock.mock(fn Tesla.Mock.mock(fn
%{ %{
method: :get, method: :get,
@ -798,26 +775,6 @@ test "should return ok if the content type is application/ld+json with the Activ
assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2") assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
end 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 test "should not return ok with other content types" do
Tesla.Mock.mock(fn Tesla.Mock.mock(fn
%{ %{

View file

@ -6,6 +6,7 @@ defmodule Pleroma.SignatureTest do
use Pleroma.DataCase, async: false use Pleroma.DataCase, async: false
@moduletag :mocked @moduletag :mocked
import ExUnit.CaptureLog
import Pleroma.Factory import Pleroma.Factory
import Tesla.Mock import Tesla.Mock
import Mock import Mock
@ -34,23 +35,25 @@ defp make_fake_conn(key_id),
do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}} do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}}
describe "fetch_public_key/1" do describe "fetch_public_key/1" do
test "it returns the key" do test "it returns key" do
expected_result = {:ok, @rsa_public_key} expected_result = {:ok, @rsa_public_key}
user = user = insert(:user, public_key: @public_key)
insert(:user)
|> with_signing_key(public_key: @public_key)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
end end
test "it returns error if public key is nil" do test "it returns error when not found user" do
# this actually needs the URL to be valid assert capture_log(fn ->
user = insert(:user) assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) ==
key_id = user.ap_id <> "#main-key" {:error, :error}
Tesla.Mock.mock(fn %{url: ^key_id} -> {:ok, %{status: 404}} end) end) =~ "[error] Could not decode user"
end
assert {:error, _} = Signature.fetch_public_key(make_fake_conn(user.ap_id)) test "it returns error if public key is nil" do
user = insert(:user, public_key: nil)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error}
end end
end end
@ -60,6 +63,12 @@ test "it returns key" do
assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key} assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key}
end 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 end
defp split_signature(sig) do defp split_signature(sig) do
@ -95,9 +104,9 @@ defp assert_part_equal(part_a, part_b) do
test "it returns signature headers" do test "it returns signature headers" do
user = user =
insert(:user, %{ insert(:user, %{
ap_id: "https://mastodon.social/users/lambadalambda" ap_id: "https://mastodon.social/users/lambadalambda",
keys: @private_key
}) })
|> with_signing_key(private_key: @private_key)
headers = %{ headers = %{
host: "test.test", host: "test.test",
@ -112,15 +121,50 @@ 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==\"" "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 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 end
describe "key_id_to_actor_id/1" do describe "key_id_to_actor_id/1" do
test "it reverses the key id to actor id" do test "it properly deduces the actor id for misskey" do
user = assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
insert(:user) {:ok, "https://example.com/users/1234"}
|> with_signing_key() end
assert Signature.key_id_to_actor_id(user.signing_key.key_id) == {:ok, user.ap_id} 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
end end
end end

View file

@ -115,15 +115,6 @@ test "verify webp files are skipped" do
assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop} assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop}
end 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 test "verify svg files are skipped" do
upload = %Pleroma.Upload{ upload = %Pleroma.Upload{
name: "sample.svg", name: "sample.svg",

View file

@ -259,7 +259,7 @@ test "works with URIs" do
|> Map.put(:multi_factor_authentication_settings, nil) |> Map.put(:multi_factor_authentication_settings, nil)
|> Map.put(:notification_settings, nil) |> Map.put(:notification_settings, nil)
assert_user_match(user, expected) assert user == expected
end end
test "excludes a blocked users from search result" do test "excludes a blocked users from search result" do

View file

@ -639,12 +639,11 @@ test "it sets the password_hash, ap_id, private key and followers collection add
changeset = User.register_changeset(%User{}, @full_user_data) changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid? assert changeset.valid?
assert is_binary(changeset.changes[:password_hash]) 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 changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
assert changeset.changes[:signing_key] assert is_binary(changeset.changes[:keys])
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" assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end end
@ -815,6 +814,7 @@ test "gets an existing user by fully qualified nickname, case insensitive" do
assert user == fetched_user assert user == fetched_user
end end
@tag capture_log: true
test "returns nil if no user could be fetched" do test "returns nil if no user could be fetched" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la") {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
assert fetched_user == "not found nonexistant@social.heldscal.la" assert fetched_user == "not found nonexistant@social.heldscal.la"
@ -871,6 +871,7 @@ 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" assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org"
end end
@tag capture_log: true
test "it returns the old user if stale, but unfetchable" do test "it returns the old user if stale, but unfetchable" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
@ -1148,18 +1149,6 @@ test "it blocks people" do
assert User.blocks?(user, blocked_user) assert User.blocks?(user, blocked_user)
end 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 test "it unblocks users" do
user = insert(:user) user = insert(:user)
blocked_user = insert(:user) blocked_user = insert(:user)
@ -1170,17 +1159,6 @@ test "it unblocks users" do
refute User.blocks?(user, blocked_user) refute User.blocks?(user, blocked_user)
end 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 test "blocks tear down cyclical follow relationships" do
blocker = insert(:user) blocker = insert(:user)
blocked = insert(:user) blocked = insert(:user)
@ -1664,6 +1642,7 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
name: "qqqqqqq", name: "qqqqqqq",
password_hash: "pdfk2$1b3n159001", password_hash: "pdfk2$1b3n159001",
keys: "RSA begin buplic key", keys: "RSA begin buplic key",
public_key: "--PRIVATE KEYE--",
avatar: %{"a" => "b"}, avatar: %{"a" => "b"},
tags: ["qqqqq"], tags: ["qqqqq"],
banner: %{"a" => "b"}, banner: %{"a" => "b"},
@ -1702,6 +1681,8 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
email: nil, email: nil,
name: nil, name: nil,
password_hash: nil, password_hash: nil,
keys: "RSA begin buplic key",
public_key: "--PRIVATE KEYE--",
avatar: %{}, avatar: %{},
tags: [], tags: [],
last_refreshed_at: nil, last_refreshed_at: nil,

View file

@ -573,6 +573,7 @@ test "it inserts an incoming activity into the database", %{conn: conn} do
assert Activity.get_by_ap_id(data["id"]) assert Activity.get_by_ap_id(data["id"])
end end
@tag capture_log: true
test "it inserts an incoming activity into the database" <> test "it inserts an incoming activity into the database" <>
"even if we can't fetch the user but have it in our db", "even if we can't fetch the user but have it in our db",
%{conn: conn} do %{conn: conn} do
@ -583,7 +584,6 @@ test "it inserts an incoming activity into the database" <>
local: false, local: false,
last_refreshed_at: nil last_refreshed_at: nil
) )
|> with_signing_key()
data = data =
File.read!("test/fixtures/mastodon-post-activity.json") File.read!("test/fixtures/mastodon-post-activity.json")
@ -594,7 +594,7 @@ test "it inserts an incoming activity into the database" <>
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"") |> put_req_header("signature", "keyId=\"#{user.ap_id}/main-key\"")
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data) |> post("/inbox", data)
@ -608,10 +608,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
sender_url = data["actor"] 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) Instances.set_consistently_unreachable(sender_url)
refute Instances.reachable?(sender_url) refute Instances.reachable?(sender_url)
@ -619,7 +616,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"") |> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"")
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data) |> post("/inbox", data)
@ -644,7 +641,7 @@ test "accept follow activity", %{conn: conn} do
assert "ok" == assert "ok" ==
conn conn
|> assign(:valid_signature, true) |> 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") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", accept) |> post("/inbox", accept)
|> json_response(200) |> json_response(200)
@ -681,7 +678,6 @@ test "accepts Add/Remove activities", %{conn: conn} do
|> String.replace("{{nickname}}", "lain") |> String.replace("{{nickname}}", "lain")
actor = "https://example.com/users/lain" actor = "https://example.com/users/lain"
key_id = "#{actor}/main-key"
insert(:user, insert(:user,
ap_id: actor, ap_id: actor,
@ -709,16 +705,6 @@ test "accepts Add/Remove activities", %{conn: conn} do
headers: [{"content-type", "application/activity+json"}] 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"} -> %{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -792,14 +778,12 @@ test "mastodon pin/unpin", %{conn: conn} do
|> String.replace("{{nickname}}", "lain") |> String.replace("{{nickname}}", "lain")
actor = "https://example.com/users/lain" actor = "https://example.com/users/lain"
key_id = "#{actor}/main-key"
sender = sender =
insert(:user, insert(:user,
ap_id: actor, ap_id: actor,
featured_address: "https://example.com/users/lain/collections/featured" featured_address: "https://example.com/users/lain/collections/featured"
) )
|> with_signing_key()
Tesla.Mock.mock(fn Tesla.Mock.mock(fn
%{ %{
@ -822,16 +806,6 @@ test "mastodon pin/unpin", %{conn: conn} do
headers: [{"content-type", "application/activity+json"}] 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"} -> %{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
@ -865,7 +839,7 @@ test "mastodon pin/unpin", %{conn: conn} do
assert "ok" == assert "ok" ==
conn conn
|> assign(:valid_signature, true) |> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"") |> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"")
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data) |> post("/inbox", data)
|> json_response(200) |> json_response(200)
@ -927,9 +901,7 @@ test "it inserts an incoming activity into the database", %{conn: conn, data: da
end end
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
user = user = insert(:user)
insert(:user)
|> with_signing_key()
data = data =
data data
@ -974,9 +946,7 @@ test "it accepts messages with cc as string instead of array", %{conn: conn, dat
end end
test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
user = user = insert(:user)
insert(:user)
|> with_signing_key()
data = data =
data data
@ -1003,10 +973,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do
user = insert(:user) user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "hey"}) {:ok, post} = CommonAPI.post(user, %{status: "hey"})
announcer = insert(:user, local: false)
announcer =
insert(:user, local: false)
|> with_signing_key()
data = %{ data = %{
"@context" => "https://www.w3.org/ns/activitystreams", "@context" => "https://www.w3.org/ns/activitystreams",
@ -1021,7 +988,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{announcer.signing_key.key_id}\"") |> put_req_header("signature", "keyId=\"#{announcer.ap_id}/main-key\"")
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -1036,10 +1003,7 @@ test "it accepts messages from actors that are followed by the user", %{
data: data data: data
} do } do
recipient = insert(:user) 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) {:ok, recipient, actor} = User.follow(recipient, actor)
@ -1055,7 +1019,7 @@ test "it accepts messages from actors that are followed by the user", %{
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data) |> post("/users/#{recipient.nickname}/inbox", data)
@ -1092,10 +1056,7 @@ test "it returns a note activity in a collection", %{conn: conn} do
end end
test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
user = user = insert(:user)
insert(:user, ap_id: data["actor"])
|> with_signing_key()
data = Map.put(data, "bcc", [user.ap_id]) data = Map.put(data, "bcc", [user.ap_id])
sender_host = URI.parse(data["actor"]).host sender_host = URI.parse(data["actor"]).host
@ -1105,7 +1066,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"") |> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"")
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -1113,9 +1074,9 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da
assert Instances.reachable?(sender_host) assert Instances.reachable?(sender_host)
end end
@tag capture_log: true
test "it removes all follower collections but actor's", %{conn: conn} do test "it removes all follower collections but actor's", %{conn: conn} do
[actor, recipient] = insert_pair(:user) [actor, recipient] = insert_pair(:user)
actor = with_signing_key(actor)
to = [ to = [
recipient.ap_id, recipient.ap_id,
@ -1144,7 +1105,7 @@ test "it removes all follower collections but actor's", %{conn: conn} do
conn conn
|> assign(:valid_signature, true) |> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data) |> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200) |> json_response(200)
@ -1177,13 +1138,10 @@ test "it requires authentication", %{conn: conn} do
assert json_response(ret_conn, 200) assert json_response(ret_conn, 200)
end end
@tag capture_log: true
test "forwarded report", %{conn: conn} do test "forwarded report", %{conn: conn} do
admin = insert(:user, is_admin: true) 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 remote_domain = URI.parse(actor.ap_id).host
reported_user = insert(:user) reported_user = insert(:user)
@ -1240,7 +1198,7 @@ test "forwarded report", %{conn: conn} do
conn conn
|> assign(:valid_signature, true) |> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"")
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data) |> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200) |> json_response(200)
@ -1257,6 +1215,7 @@ test "forwarded report", %{conn: conn} do
) )
end end
@tag capture_log: true
test "forwarded report from mastodon", %{conn: conn} do test "forwarded report from mastodon", %{conn: conn} do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
actor = insert(:user, local: false) actor = insert(:user, local: false)
@ -1273,22 +1232,12 @@ test "forwarded report from mastodon", %{conn: conn} do
|> File.read!() |> File.read!()
|> String.replace("{{DOMAIN}}", remote_domain) |> String.replace("{{DOMAIN}}", remote_domain)
key_url = "#{remote_actor}#main-key" Tesla.Mock.mock(fn %{url: ^remote_actor} ->
%Tesla.Env{
Tesla.Mock.mock(fn status: 200,
%{url: ^remote_actor} -> body: mock_json_body,
%Tesla.Env{ headers: [{"content-type", "application/activity+json"}]
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) end)
data = %{ data = %{
@ -1305,7 +1254,7 @@ test "forwarded report from mastodon", %{conn: conn} do
conn conn
|> assign(:valid_signature, true) |> 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") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data) |> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200) |> json_response(200)

View file

@ -140,9 +140,7 @@ test "publish to url with with different ports" do
{:ok, %Tesla.Env{status: 200, body: "port 80"}} {:ok, %Tesla.Env{status: 200, body: "port 80"}}
end) end)
actor = actor = insert(:user)
insert(:user)
|> with_signing_key()
assert {:ok, %{body: "port 42"}} = assert {:ok, %{body: "port 42"}} =
Publisher.publish_one(%{ Publisher.publish_one(%{
@ -167,10 +165,7 @@ test "publish to url with with different ports" do
Instances, Instances,
[:passthrough], [:passthrough],
[] do [] do
actor = actor = insert(:user)
insert(:user)
|> with_signing_key()
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
@ -181,10 +176,7 @@ test "publish to url with with different ports" do
Instances, Instances,
[:passthrough], [:passthrough],
[] do [] do
actor = actor = insert(:user)
insert(:user)
|> with_signing_key()
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = assert {:ok, _} =
@ -203,10 +195,7 @@ test "publish to url with with different ports" do
Instances, Instances,
[:passthrough], [:passthrough],
[] do [] do
actor = actor = insert(:user)
insert(:user)
|> with_signing_key()
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = assert {:ok, _} =
@ -225,10 +214,7 @@ test "publish to url with with different ports" do
Instances, Instances,
[:passthrough], [:passthrough],
[] do [] do
actor = actor = insert(:user)
insert(:user)
|> with_signing_key()
inbox = "http://404.site/users/nick1/inbox" inbox = "http://404.site/users/nick1/inbox"
assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
@ -240,10 +226,7 @@ test "publish to url with with different ports" do
Instances, Instances,
[:passthrough], [:passthrough],
[] do [] do
actor = actor = insert(:user)
insert(:user)
|> with_signing_key()
inbox = "http://connrefused.site/users/nick1/inbox" inbox = "http://connrefused.site/users/nick1/inbox"
assert capture_log(fn -> assert capture_log(fn ->
@ -258,10 +241,7 @@ test "publish to url with with different ports" do
Instances, Instances,
[:passthrough], [:passthrough],
[] do [] do
actor = actor = insert(:user)
insert(:user)
|> with_signing_key()
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
@ -273,10 +253,7 @@ test "publish to url with with different ports" do
Instances, Instances,
[:passthrough], [:passthrough],
[] do [] do
actor = actor = insert(:user)
insert(:user)
|> with_signing_key()
inbox = "http://connrefused.site/users/nick1/inbox" inbox = "http://connrefused.site/users/nick1/inbox"
assert capture_log(fn -> assert capture_log(fn ->
@ -317,9 +294,7 @@ test "publish to url with with different ports" do
ap_enabled: true ap_enabled: true
}) })
actor = actor = insert(:user, follower_address: follower.ap_id)
insert(:user, follower_address: follower.ap_id)
|> with_signing_key()
{:ok, follower, actor} = Pleroma.User.follow(follower, actor) {:ok, follower, actor} = Pleroma.User.follow(follower, actor)
{:ok, _another_follower, actor} = Pleroma.User.follow(another_follower, actor) {:ok, _another_follower, actor} = Pleroma.User.follow(another_follower, actor)
@ -390,9 +365,7 @@ test "publish to url with with different ports" do
ap_enabled: true ap_enabled: true
}) })
actor = actor = insert(:user, follower_address: follower.ap_id)
insert(:user, follower_address: follower.ap_id)
|> with_signing_key()
{:ok, follower, actor} = Pleroma.User.follow(follower, actor) {:ok, follower, actor} = Pleroma.User.follow(follower, actor)
actor = refresh_record(actor) actor = refresh_record(actor)

View file

@ -114,6 +114,7 @@ test "returns error when activity not `Create` type" do
assert Relay.publish(activity) == {:error, "Not implemented"} assert Relay.publish(activity) == {:error, "Not implemented"}
end end
@tag capture_log: true
test "returns error when activity not public" do test "returns error when activity not public" do
activity = insert(:direct_note_activity) activity = insert(:direct_note_activity)
assert Relay.publish(activity) == {:error, false} assert Relay.publish(activity) == {:error, false}

View file

@ -83,6 +83,7 @@ test "it works for incoming announces, fetching the announced object" do
assert(Activity.get_create_by_object_ap_id(data["object"])) assert(Activity.get_create_by_object_ap_id(data["object"]))
end end
@tag capture_log: true
test "it works for incoming announces with an existing activity" do test "it works for incoming announces with an existing activity" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"}) {:ok, activity} = CommonAPI.post(user, %{status: "hey"})
@ -135,6 +136,7 @@ test "it works for incoming announces with an inlined activity" do
assert object.data["content"] == "this is a private toot" assert object.data["content"] == "this is a private toot"
end end
@tag capture_log: true
test "it rejects incoming announces with an inlined activity from another origin" do test "it rejects incoming announces with an inlined activity from another origin" do
Tesla.Mock.mock(fn Tesla.Mock.mock(fn
%{method: :get} -> %Tesla.Env{status: 404, body: ""} %{method: :get} -> %Tesla.Env{status: 404, body: ""}

View file

@ -86,6 +86,7 @@ test "it fails for incoming deletes with spoofed origin" do
assert match?({:error, _}, Transmogrifier.handle_incoming(data)) assert match?({:error, _}, Transmogrifier.handle_incoming(data))
end end
@tag capture_log: true
test "it works for incoming user deletes" do test "it works for incoming user deletes" do
%{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")

View file

@ -57,6 +57,7 @@ test "it ignores an incoming notice if we already have it" do
assert activity == returned_activity assert activity == returned_activity
end end
@tag capture_log: true
test "it fetches reply-to activities if we don't have them" do test "it fetches reply-to activities if we don't have them" do
data = data =
File.read!("test/fixtures/mastodon-post-activity.json") File.read!("test/fixtures/mastodon-post-activity.json")
@ -536,6 +537,7 @@ test "returns object with inReplyTo when denied incoming reply", %{data: data} d
assert modified_object["inReplyTo"] == [] assert modified_object["inReplyTo"] == []
end end
@tag capture_log: true
test "returns modified object when allowed incoming reply", %{data: data} do test "returns modified object when allowed incoming reply", %{data: data} do
object_with_reply = object_with_reply =
Map.put( Map.put(
@ -698,7 +700,7 @@ test "take_emoji_tags/1" do
assert Transmogrifier.take_emoji_tags(user) == [ assert Transmogrifier.take_emoji_tags(user) == [
%{ %{
"icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"}, "icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"},
"id" => nil, "id" => "https://example.org/firefox.png",
"name" => ":firefox:", "name" => ":firefox:",
"type" => "Emoji", "type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z" "updated" => "1970-01-01T00:00:00Z"

View file

@ -561,6 +561,7 @@ test "returns nil when cannot normalize object" do
end) =~ ":valid_uri_scheme" end) =~ ":valid_uri_scheme"
end end
@tag capture_log: true
test "returns {:ok, %Object{}} for success case" do test "returns {:ok, %Object{}} for success case" do
assert {:ok, %Object{}} = assert {:ok, %Object{}} =
Transmogrifier.get_obj_helper( Transmogrifier.get_obj_helper(

View file

@ -11,9 +11,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
test "Renders a user, including the public key" do test "Renders a user, including the public key" do
user = user = insert(:user)
insert(:user)
|> with_signing_key()
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
@ -39,15 +37,13 @@ test "Renders profile fields" do
end end
test "Renders with emoji tags" do test "Renders with emoji tags" do
user = user = insert(:user, emoji: %{"bib" => "/test"})
insert(:user, emoji: %{"bib" => "/test"})
|> with_signing_key()
assert %{ assert %{
"tag" => [ "tag" => [
%{ %{
"icon" => %{"type" => "Image", "url" => "/test"}, "icon" => %{"type" => "Image", "url" => "/test"},
"id" => nil, "id" => "/test",
"name" => ":bib:", "name" => ":bib:",
"type" => "Emoji", "type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z" "updated" => "1970-01-01T00:00:00Z"
@ -78,18 +74,13 @@ test "Does not add an avatar image if the user hasn't set one" do
end end
test "renders an invisible user with the invisible property set to true" do test "renders an invisible user with the invisible property set to true" do
user = user = insert(:user, invisible: true)
insert(:user, invisible: true)
|> with_signing_key()
assert %{"invisible" => true} = UserView.render("service.json", %{user: user}) assert %{"invisible" => true} = UserView.render("service.json", %{user: user})
end end
test "service has a few essential fields" do test "service has a few essential fields" do
user = user = insert(:user)
insert(:user)
|> with_signing_key()
result = UserView.render("service.json", %{user: user}) result = UserView.render("service.json", %{user: user})
assert result["id"] assert result["id"]
assert result["type"] == "Application" assert result["type"] == "Application"
@ -129,9 +120,7 @@ test "remote users have an empty endpoints structure" do
end end
test "instance users do not expose oAuth endpoints" do test "instance users do not expose oAuth endpoints" do
user = user = insert(:user, nickname: nil, local: true)
insert(:user, nickname: nil, local: true)
|> with_signing_key()
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})

View file

@ -79,6 +79,7 @@ test "search", %{conn: conn} do
assert status["id"] == to_string(activity.id) assert status["id"] == to_string(activity.id)
end end
@tag capture_log: true
test "constructs hashtags from search query", %{conn: conn} do test "constructs hashtags from search query", %{conn: conn} do
results = results =
conn conn

View file

@ -152,6 +152,7 @@ test "filtering", %{conn: conn, user: user} do
end end
describe "public" do describe "public" do
@tag capture_log: true
test "the public timeline", %{conn: conn} do test "the public timeline", %{conn: conn} do
user = insert(:user) user = insert(:user)
@ -809,6 +810,7 @@ test "filtering", %{user: user, conn: conn} do
describe "hashtag" do describe "hashtag" do
setup do: oauth_access(["n/a"]) setup do: oauth_access(["n/a"])
@tag capture_log: true
test "hashtag timeline", %{conn: conn} do test "hashtag timeline", %{conn: conn} do
following = insert(:user) following = insert(:user)

View file

@ -43,7 +43,7 @@ test "renders a poll" do
%{title: "why are you even asking?", votes_count: 0} %{title: "why are you even asking?", votes_count: 0}
], ],
votes_count: 0, votes_count: 0,
voters_count: nil voters_count: 0
} }
result = PollView.render("show.json", %{object: object}) result = PollView.render("show.json", %{object: object})

View file

@ -33,10 +33,6 @@ test "has an emoji reaction list" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
third_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, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "") {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "")
@ -44,8 +40,6 @@ test "has an emoji reaction list" do
{:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") {: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, "")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:") {: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) activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity) status = StatusView.render("show.json", activity: activity)
@ -61,8 +55,7 @@ test "has an emoji reaction list" do
url: "http://localhost:4001/emoji/dino walking.gif", url: "http://localhost:4001/emoji/dino walking.gif",
account_ids: [other_user.id, user.id] 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) status = StatusView.render("show.json", activity: activity, for: user)
@ -80,8 +73,6 @@ test "has an emoji reaction list" do
}, },
%{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} %{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 end
test "works correctly with badly formatted emojis" do test "works correctly with badly formatted emojis" do

View file

@ -14,15 +14,6 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
import Phoenix.Controller, only: [put_format: 2] import Phoenix.Controller, only: [put_format: 2]
import Mock 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([ setup_with_mocks([
{HTTPSignatures, [], {HTTPSignatures, [],
[ [
@ -55,15 +46,15 @@ defp submit_to_plug(host, method, path) do
|> HTTPSignaturePlug.call(%{}) |> HTTPSignaturePlug.call(%{})
end end
test "it call HTTPSignatures to check validity if the actor signed it", %{user: user} do test "it call HTTPSignatures to check validity if the actor signed it" do
params = %{"actor" => user.ap_id} params = %{"actor" => "http://mastodon.example.org/users/admin"}
conn = build_conn(:get, "/doesntmattter", params) conn = build_conn(:get, "/doesntmattter", params)
conn = conn =
conn conn
|> put_req_header( |> put_req_header(
"signature", "signature",
"keyId=\"#{user.signing_key.key_id}\"" "keyId=\"http://mastodon.example.org/users/admin#main-key"
) )
|> put_format("activity+json") |> put_format("activity+json")
|> HTTPSignaturePlug.call(%{}) |> HTTPSignaturePlug.call(%{})

View file

@ -8,63 +8,52 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
import Tesla.Mock import Tesla.Mock
import Plug.Conn import Plug.Conn
import Pleroma.Factory
import Pleroma.Tests.Helpers, only: [clear_config: 2] import Pleroma.Tests.Helpers, only: [clear_config: 2]
setup do setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end) mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
user =
insert(:user)
|> with_signing_key()
{:ok, %{user: user}}
end end
defp set_signature(conn, ap_id) do defp set_signature(conn, key_id) do
conn conn
|> put_req_header("signature", "keyId=\"#{ap_id}#main-key\"") |> put_req_header("signature", "keyId=\"#{key_id}\"")
|> assign(:valid_signature, true) |> assign(:valid_signature, true)
end end
test "it successfully maps a valid identity with a valid signature", %{user: user} do test "it successfully maps a valid identity with a valid signature" do
conn = conn =
build_conn(:get, "/doesntmattter") build_conn(:get, "/doesntmattter")
|> set_signature(user.ap_id) |> set_signature("http://mastodon.example.org/users/admin")
|> MappedSignatureToIdentityPlug.call(%{}) |> MappedSignatureToIdentityPlug.call(%{})
refute is_nil(conn.assigns.user) refute is_nil(conn.assigns.user)
end end
test "it successfully maps a valid identity with a valid signature with payload", %{user: user} do test "it successfully maps a valid identity with a valid signature with payload" do
conn = conn =
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|> set_signature(user.ap_id) |> set_signature("http://mastodon.example.org/users/admin")
|> MappedSignatureToIdentityPlug.call(%{}) |> MappedSignatureToIdentityPlug.call(%{})
refute is_nil(conn.assigns.user) refute is_nil(conn.assigns.user)
end end
test "it considers a mapped identity to be invalid when it mismatches a payload", %{user: user} do test "it considers a mapped identity to be invalid when it mismatches a payload" do
conn = conn =
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|> set_signature("https://niu.moe/users/rye") |> set_signature("https://niu.moe/users/rye")
|> MappedSignatureToIdentityPlug.call(%{}) |> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns assert %{valid_signature: false} == conn.assigns
end end
test "it considers a mapped identity to be invalid when the associated instance is blocked", %{ test "it considers a mapped identity to be invalid when the associated instance is blocked" do
user: user
} do
clear_config([:activitypub, :authorized_fetch_mode], true) clear_config([:activitypub, :authorized_fetch_mode], true)
# extract domain from user.ap_id
url = URI.parse(user.ap_id)
clear_config([:mrf_simple, :reject], [ clear_config([:mrf_simple, :reject], [
{url.host, "anime is banned"} {"mastodon.example.org", "anime is banned"}
]) ])
on_exit(fn -> on_exit(fn ->
@ -73,21 +62,18 @@ test "it considers a mapped identity to be invalid when the associated instance
end) end)
conn = conn =
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|> set_signature(user.ap_id) |> set_signature("http://mastodon.example.org/users/admin")
|> MappedSignatureToIdentityPlug.call(%{}) |> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns assert %{valid_signature: false} == conn.assigns
end end
test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed", test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed" do
%{user: user} do
clear_config([:activitypub, :authorized_fetch_mode], true) clear_config([:activitypub, :authorized_fetch_mode], true)
url = URI.parse(user.ap_id)
clear_config([:mrf_simple, :accept], [ clear_config([:mrf_simple, :accept], [
{url.host, "anime is allowed"} {"mastodon.example.org", "anime is allowed"}
]) ])
on_exit(fn -> on_exit(fn ->
@ -96,16 +82,15 @@ test "allowlist federation: it considers a mapped identity to be valid when the
end) end)
conn = conn =
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|> set_signature(user.ap_id) |> set_signature("http://mastodon.example.org/users/admin")
|> MappedSignatureToIdentityPlug.call(%{}) |> MappedSignatureToIdentityPlug.call(%{})
assert conn.assigns[:valid_signature] assert conn.assigns[:valid_signature]
refute is_nil(conn.assigns.user) refute is_nil(conn.assigns.user)
end end
test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed", test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed" do
%{user: user} do
clear_config([:activitypub, :authorized_fetch_mode], true) clear_config([:activitypub, :authorized_fetch_mode], true)
clear_config([:mrf_simple, :accept], [ clear_config([:mrf_simple, :accept], [
@ -118,8 +103,8 @@ test "allowlist federation: it considers a mapped identity to be invalid when th
end) end)
conn = conn =
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|> set_signature(user.ap_id) |> set_signature("http://mastodon.example.org/users/admin")
|> MappedSignatureToIdentityPlug.call(%{}) |> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns assert %{valid_signature: false} == conn.assigns

View file

@ -67,6 +67,7 @@ test "performs sending notifications" do
assert Impl.perform(notif) == {:ok, [:ok, :ok]} assert Impl.perform(notif) == {:ok, [:ok, :ok]}
end end
@tag capture_log: true
test "returns error if notif does not match " do test "returns error if notif does not match " do
assert Impl.perform(%{}) == {:error, :unknown_type} assert Impl.perform(%{}) == {:error, :unknown_type}
end end
@ -75,6 +76,7 @@ test "successful message sending" do
assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok
end end
@tag capture_log: true
test "fail message sending" do test "fail message sending" do
assert Impl.push_message( assert Impl.push_message(
@message, @message,

View file

@ -190,6 +190,7 @@ test "prevents spoofing" do
end end
end end
@tag capture_log: true
test "prevents forgeries" do test "prevents forgeries" do
Tesla.Mock.mock(fn Tesla.Mock.mock(fn
%{url: "https://bad.com/.well-known/webfinger?resource=acct:meanie@bad.com"} -> %{url: "https://bad.com/.well-known/webfinger?resource=acct:meanie@bad.com"} ->

View file

@ -47,6 +47,7 @@ def instance_factory(attrs \\ %{}) do
end end
def user_factory(attrs \\ %{}) do def user_factory(attrs \\ %{}) do
pem = Enum.random(@rsa_keys)
# Argon2.hash_pwd_salt("test") # Argon2.hash_pwd_salt("test")
# it really eats CPU time, so we use a precomputed hash # it really eats CPU time, so we use a precomputed hash
password_hash = password_hash =
@ -63,7 +64,8 @@ def user_factory(attrs \\ %{}) do
last_refreshed_at: NaiveDateTime.utc_now(), last_refreshed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{}, notification_settings: %Pleroma.User.NotificationSetting{},
multi_factor_authentication_settings: %Pleroma.MFA.Settings{}, multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
ap_enabled: true ap_enabled: true,
keys: pem
} }
urls = urls =
@ -95,28 +97,6 @@ def user_factory(attrs \\ %{}) do
|> merge_attributes(attrs) |> merge_attributes(attrs)
end 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 def user_relationship_factory(attrs \\ %{}) do
source = attrs[:source] || insert(:user) source = attrs[:source] || insert(:user)
target = attrs[:target] || insert(:user) target = attrs[:target] || insert(:user)

View file

@ -66,8 +66,6 @@ defmacro __using__(_opts) do
clear_config: 2 clear_config: 2
] ]
import Pleroma.Test.MatchingHelpers
def time_travel(entity, seconds) do def time_travel(entity, seconds) do
new_time = NaiveDateTime.add(entity.inserted_at, seconds) new_time = NaiveDateTime.add(entity.inserted_at, seconds)

View file

@ -263,12 +263,7 @@ def get("https://n1u.moe/users/rye", _, _, @activitypub_accept_headers) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
status: 200, status: 200,
body: body: File.read!("test/fixtures/tesla_mock/rye.json"),
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() headers: activitypub_object_headers()
}} }}
end end
@ -424,15 +419,6 @@ def get("http://mastodon.example.org/users/admin", _, _, _) do
}} }}
end 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( def get(
"http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true", "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true",
_, _,
@ -967,15 +953,6 @@ def get("https://mastodon.social/users/lambadalambda", _, _, _) do
}} }}
end 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 def get("https://mastodon.social/users/lambadalambda/collections/featured", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{
@ -1421,15 +1398,6 @@ def get("https://relay.mastodon.host/actor", _, _, _) do
}} }}
end 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 def get("http://localhost:4001/", _, "", [{"accept", "text/html"}]) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}} {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}}
end end

View file

@ -1,10 +0,0 @@
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

View file

@ -3,11 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: [] 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) Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)