Compare commits

...

32 commits

Author SHA1 Message Date
67f3ee45b4 Merge branch 'develop' of https://akkoma.dev/AkkomaGang/akkoma into akko.wtf 2024-10-31 19:01:11 -04:00
c0a99df06a Merge remote-tracking branch 'oneric/varfixes' into develop 2024-10-30 15:15:00 +00:00
0cb4c35ee4 Merge pull request 'Extract keys to their own table, match keyID' (#816) from keys-extraction into develop
Reviewed-on: AkkomaGang/akkoma#816
2024-10-30 15:08:11 +00:00
c9b3fcc1d3 allow for OTP code changes in :zip 2024-10-30 14:43:18 +00:00
11c5838947 standardise local key id generation 2024-10-30 12:44:01 +00:00
180dc8b472 downgrade earmark 2024-10-26 08:50:38 +01:00
d330c57cda make sure we correctly match key objects 2024-10-26 08:42:07 +01:00
bd64d07082 ensure migration actually works 2024-10-26 07:51:41 +01:00
9d2c558f64 remove unused import 2024-10-26 07:42:43 +01:00
ac25b051ae remove previous "allow user routes" functionality 2024-10-26 07:28:43 +01:00
c5a44a59db remove unneeded index 2024-10-26 07:00:38 +01:00
58d5d9d7bf fix tests, contain object 2024-10-26 06:58:47 +01:00
b6e8fde4dd Merge branch 'develop' into keys-extraction 2024-10-26 06:11:29 +01:00
13215f5f06 remove public key field 2024-10-26 05:28:55 +01:00
430b376ded mix format 2024-10-26 05:05:48 +01:00
ccf1007883 Fix about a million tests 2024-10-26 05:05:48 +01:00
6da783b84d Fix http signature plug tests 2024-10-26 05:05:48 +01:00
8f322456a0 Allow unsigned fetches of a user's public key 2024-10-26 05:05:48 +01:00
9c876cea21 Fix some tests 2024-10-26 05:05:48 +01:00
9728e2f8f7 adjust logic to use relation :signing_key 2024-10-26 05:05:47 +01:00
b0f7da9ce0 remove now-unused Keys module 2024-10-26 05:05:28 +01:00
fc99c694e6 Add signing key modules 2024-10-26 05:05:28 +01:00
a3101a435b Fix swagger-ui
Ever since the browser frontend switcher was introduced in
de64c6c54a /akkoma counts as
an API prefix and thus gets skipped by frontend plugs
breaking the old swagger ui path of /akkoma/swagger-ui.

Do the simple thing and change the frontend path to
/pleroma/swaggerui which isn't an API path and can't collide
with frontend user paths given pleroma is areserved nickname.

Reported in
  https://meta.akkoma.dev/t/view-all-endpoints/269/7
  https://meta.akkoma.dev/t/swagger-ui-not-loading/728
2024-06-27 18:29:45 +02:00
d488cf476e Fix voters count field
Mastodon API demands this be null unless it’s a multi-selection poll.
Not abiding by this can mess up display in some clients.

Fixes: AkkomaGang/akkoma#190
2024-06-27 18:29:45 +02:00
ca182a0ae7 Correctly parse content types with multiple profiles
Multiple profiles can be specified as a space-separated list
and the possibility of additional profiles is explicitly brought up
in ActivityStream spec
2024-06-27 18:29:45 +02:00
495a1a71e8 strip_metadata: skip BMP files
Not _yet_ supported as of exiftool 12.87, though
at first glance it seems like standard BMP files
can't store any metadata besides colour profiles

Fixes the specific case from
AkkomaGang/akkoma-fe#396
although the frontend shouldn’t get bricked regardless.
2024-06-27 18:29:45 +02:00
cf19d4901f Disable Ecto query logging in tests
The debug logs are very noisy and can be enabled during analysis
of a specific error believed to be SQL-related

--

Before log capturing those debug messages were still hidden,
but with log capturing they show up in the output of failed
tests unless disabled.

Cherry-picked-from: e628d00a81
2024-06-27 18:29:45 +02:00
07539f7825 Hide logs during test unless a test fails
Currently `mix test` prints a slew of logs in the terminal
with messages from different tests intermsparsed. Globally
enabling capture log hides log messages unless a test fails
reducing noise and making it easier to anylse the important
(from failed tests) messages.

Compiler warnings and a few messages not printed via Logger
still show up but its much more readable than before.

Ported from: 3aed111a42
2024-06-27 18:29:45 +02:00
0ab2f2ab45 ci: retry failed tasks once
We have a bunch of mysterious sporadic failures which usually disappear
when rerunning failed jobs only. Ideally we should locate and fix the
cause of those psoradic failures, but until we figure this out retrying
once makes CI status less useless.
2024-06-27 18:29:45 +02:00
95ed4931f8 docs: note frontend tasks need to be run as akkoma user
Fixes: AkkomaGang/akkoma#748
2024-06-27 18:29:45 +02:00
7cd3954152 Remove superfluous actor key suffix
Fragments are already always stripped anyway
so listing one specific fragment here is
unnecessary and potentially confusing.

This effectively reverts
4457928e32
but keeps the added bridgy testcase.
2024-06-27 18:29:45 +02:00
80a4e30be7 Upgrade captach dep
Fixes a deprecation warning showing up each mix call
when using elixir 1.17
2024-06-27 18:29:45 +02:00
56 changed files with 870 additions and 404 deletions

View file

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

View file

@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## BREAKING
- Minimum PostgreSQL version is raised to 12
- Swagger UI moved from `/akkoma/swaggerui/` to `/pleroma/swaggerui/`
## Added
- Implement [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) (federation documentation)
@ -19,6 +20,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Meilisearch: order of results returned from our REST API now actually matches how Meilisearch ranks results
- Emoji are now federated as anonymous objects, fixing issues with
some strict servers e.g. rejecting e.g. remote emoji reactions
- AP objects with additional JSON-LD profiles beyond ActivityStreams can now be fetched
- Single-selection polls no longer expose the voter_count; MastoAPI demands it be null
and this confused some clients leading to vote distributions >100%
## Changed
- Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated.

View file

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

View file

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

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

View file

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

View file

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

View file

@ -369,8 +369,12 @@ def get_object(id) do
{"activity+json", _} ->
{:ok, final_id, body}
{"ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
{:ok, final_id, body}
{"ld+json", %{"profile" => profiles}} ->
if "https://www.w3.org/ns/activitystreams" in String.split(profiles) do
{:ok, final_id, body}
else
{:error, {:content_type, content_type}}
end
_ ->
{:error, {:content_type, content_type}}

View file

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

View file

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

View file

@ -25,7 +25,6 @@ defmodule Pleroma.User do
alias Pleroma.Hashtag
alias Pleroma.User.HashtagFollow
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA
alias Pleroma.Notification
alias Pleroma.Object
@ -43,6 +42,7 @@ defmodule Pleroma.User do
alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe
alias Pleroma.Workers.BackgroundWorker
alias Pleroma.User.SigningKey
use Pleroma.Web, :verified_routes
@ -101,7 +101,6 @@ defmodule Pleroma.User do
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
field(:public_key, :string)
field(:ap_id, :string)
field(:avatar, :map, default: %{})
field(:local, :boolean, default: true)
@ -222,6 +221,10 @@ defmodule Pleroma.User do
on_replace: :delete
)
# FOR THE FUTURE: We might want to make this a one-to-many relationship
# it's entirely possible right now, but we don't have a use case for it
has_one(:signing_key, SigningKey, foreign_key: :user_id)
timestamps()
end
@ -457,6 +460,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|> fix_follower_address()
struct
|> Repo.preload(:signing_key)
|> cast(
params,
[
@ -466,7 +470,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:inbox,
:shared_inbox,
:nickname,
:public_key,
:avatar,
:ap_enabled,
:banner,
@ -495,6 +498,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|> validate_required([:ap_id])
|> validate_required([:name], trim: false)
|> unique_constraint(:nickname)
|> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, required: false)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
@ -526,7 +530,6 @@ def update_changeset(struct, params \\ %{}) do
:name,
:emoji,
:avatar,
:public_key,
:inbox,
:shared_inbox,
:is_locked,
@ -570,6 +573,7 @@ def update_changeset(struct, params \\ %{}) do
:pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
)
|> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, requred: false)
|> validate_fields(false, struct)
end
@ -828,8 +832,10 @@ def put_following_and_follower_and_featured_address(changeset) do
end
defp put_private_key(changeset) do
{:ok, pem} = Keys.generate_rsa_pem()
put_change(changeset, :keys, pem)
ap_id = get_field(changeset, :ap_id)
changeset
|> put_assoc(:signing_key, SigningKey.generate_local_keys(ap_id))
end
defp autofollow_users(user) do
@ -1146,7 +1152,8 @@ def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
was_superuser_before_update = User.superuser?(user)
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user)
user
|> set_cache()
end
|> maybe_remove_report_notifications(was_superuser_before_update)
end
@ -2051,24 +2058,16 @@ defp create_service_actor(uri, nickname) do
|> set_cache()
end
def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
key =
public_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
end
def public_key(_), do: {:error, "key not found"}
defdelegate public_key(user), to: SigningKey
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key(user) do
{:ok, public_key} <- SigningKey.public_key(user) do
{:ok, public_key}
else
_ -> :error
e ->
Logger.error("Could not get public key for #{ap_id}.\n#{inspect(e)}")
{:error, e}
end
end

View file

@ -0,0 +1,262 @@
defmodule Pleroma.User.SigningKey do
use Ecto.Schema
import Ecto.Query
import Ecto.Changeset
require Pleroma.Constants
alias Pleroma.User
alias Pleroma.Repo
require Logger
@primary_key false
schema "signing_keys" do
belongs_to(:user, Pleroma.User, type: FlakeId.Ecto.CompatType)
field :public_key, :string
field :private_key, :string
# This is an arbitrary field given by the remote instance
field :key_id, :string, primary_key: true
timestamps()
end
def load_key(%User{} = user) do
user
|> Repo.preload(:signing_key)
end
def key_id_of_local_user(%User{local: true} = user) do
case Repo.preload(user, :signing_key) do
%User{signing_key: %__MODULE__{key_id: key_id}} -> key_id
_ -> nil
end
end
@spec remote_changeset(__MODULE__, map) :: Changeset.t()
def remote_changeset(%__MODULE__{} = signing_key, attrs) do
signing_key
|> cast(attrs, [:public_key, :key_id])
|> validate_required([:public_key, :key_id])
end
@spec key_id_to_user_id(String.t()) :: String.t() | nil
@doc """
Given a key ID, return the user ID associated with that key.
Returns nil if the key ID is not found.
"""
def key_id_to_user_id(key_id) do
from(sk in __MODULE__, where: sk.key_id == ^key_id)
|> select([sk], sk.user_id)
|> Repo.one()
end
@spec key_id_to_ap_id(String.t()) :: String.t() | nil
@doc """
Given a key ID, return the AP ID associated with that key.
Returns nil if the key ID is not found.
"""
def key_id_to_ap_id(key_id) do
Logger.debug("Looking up key ID: #{key_id}")
result =
from(sk in __MODULE__, where: sk.key_id == ^key_id)
|> join(:inner, [sk], u in User, on: sk.user_id == u.id)
|> select([sk, u], %{user: u})
|> Repo.one()
case result do
%{user: %User{ap_id: ap_id}} -> ap_id
_ -> nil
end
end
@spec generate_rsa_pem() :: {:ok, binary()}
@doc """
Generate a new RSA private key and return it as a PEM-encoded string.
"""
def generate_rsa_pem do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
{:ok, pem}
end
@spec generate_local_keys(String.t()) :: {:ok, Changeset.t()} | {:error, String.t()}
@doc """
Generate a new RSA key pair and create a changeset for it
"""
def generate_local_keys(ap_id) do
{:ok, private_pem} = generate_rsa_pem()
{:ok, local_pem} = private_pem_to_public_pem(private_pem)
%__MODULE__{}
|> change()
|> put_change(:public_key, local_pem)
|> put_change(:private_key, private_pem)
|> put_change(:key_id, local_key_id(ap_id))
end
@spec local_key_id(String.t()) :: String.t()
@doc """
Given an AP ID, return the key ID for the local user.
"""
def local_key_id(ap_id) do
ap_id <> "#main-key"
end
@spec private_pem_to_public_pem(binary) :: {:ok, binary()} | {:error, String.t()}
@doc """
Given a private key in PEM format, return the corresponding public key in PEM format.
"""
def private_pem_to_public_pem(private_pem) do
[private_key_code] = :public_key.pem_decode(private_pem)
private_key = :public_key.pem_entry_decode(private_key_code)
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
public_key = {:RSAPublicKey, modulus, exponent}
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
{:ok, :public_key.pem_encode([public_key])}
end
@spec public_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
@doc """
Given a user, return the public key for that user in binary format.
"""
def public_key(%User{} = user) do
case Repo.preload(user, :signing_key) do
%User{signing_key: %__MODULE__{public_key: public_key_pem}} ->
key =
public_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
_ ->
{:error, "key not found"}
end
end
def public_key(_), do: {:error, "key not found"}
def public_key_pem(%User{} = user) do
case Repo.preload(user, :signing_key) do
%User{signing_key: %__MODULE__{public_key: public_key_pem}} -> {:ok, public_key_pem}
_ -> {:error, "key not found"}
end
end
def public_key_pem(_e) do
{:error, "key not found"}
end
@spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
@doc """
Given a user, return the private key for that user in binary format.
"""
def private_key(%User{} = user) do
case Repo.preload(user, :signing_key) do
%{signing_key: %__MODULE__{private_key: private_key_pem}} ->
key =
private_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
_ ->
{:error, "key not found"}
end
end
@spec get_or_fetch_by_key_id(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
@doc """
Given a key ID, return the signing key associated with that key.
Will either return the key if it exists locally, or fetch it from the remote instance.
"""
def get_or_fetch_by_key_id(key_id) do
case key_id_to_user_id(key_id) do
nil ->
fetch_remote_key(key_id)
user_id ->
{:ok, Repo.get_by(__MODULE__, user_id: user_id)}
end
end
@spec fetch_remote_key(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
@doc """
Fetch a remote key by key ID.
Will send a request to the remote instance to get the key ID.
This request should, at the very least, return a user ID and a public key object.
Though bear in mind that some implementations (looking at you, pleroma) may require a signature for this request.
This has the potential to create an infinite loop if the remote instance requires a signature to fetch the key...
So if we're rejected, we should probably just give up.
"""
def fetch_remote_key(key_id) do
Logger.debug("Fetching remote key: #{key_id}")
resp = Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(key_id)
case resp do
{:ok, _body} ->
case handle_signature_response(resp) do
{:ok, ap_id, public_key_pem} ->
Logger.debug("Fetched remote key: #{ap_id}")
# fetch the user
{:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
# store the key
key = %__MODULE__{
user_id: user.id,
public_key: public_key_pem,
key_id: key_id
}
Repo.insert(key, on_conflict: :replace_all, conflict_target: :key_id)
e ->
Logger.debug("Failed to fetch remote key: #{inspect(e)}")
{:error, "Could not fetch key"}
end
_ ->
Logger.debug("Failed to fetch remote key: #{inspect(resp)}")
{:error, "Could not fetch key"}
end
end
# Take the response from the remote instance and extract the key details
# will check if the key ID matches the owner of the key, if not, error
defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do
if ap_id !== public_key["owner"] do
{:error, "Key ID does not match owner"}
else
%{"publicKeyPem" => public_key_pem} = public_key
{:ok, ap_id, public_key_pem}
end
end
defp handle_signature_response({:ok, body}) do
case body do
%{
"type" => "CryptographicKey",
"publicKeyPem" => public_key_pem,
"owner" => ap_id
} ->
{:ok, ap_id, public_key_pem}
# for when we get a subset of the user object
%{
"id" => _user_id,
"publicKey" => _public_key,
"type" => actor_type
}
when actor_type in Pleroma.Constants.actor_types() ->
extract_key_details(body)
%{"error" => error} ->
{:error, error}
end
end
defp handle_signature_response({:error, e}), do: {:error, e}
defp handle_signature_response(other), do: {:error, "Could not fetch key: #{inspect(other)}"}
end

View file

@ -1547,6 +1547,17 @@ defp normalize_attachment(%{} = attachment), do: [attachment]
defp normalize_attachment(attachment) when is_list(attachment), do: attachment
defp normalize_attachment(_), do: []
defp maybe_make_public_key_object(data) do
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
%{
public_key: data["publicKey"]["publicKeyPem"],
key_id: data["publicKey"]["id"]
}
else
nil
end
end
defp object_to_user_data(data, additional) do
fields =
data
@ -1578,9 +1589,16 @@ defp object_to_user_data(data, additional) do
featured_address = data["featured"]
{:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address)
public_key =
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
data["publicKey"]["publicKeyPem"]
# first, check that the owner is correct
signing_key =
if data["id"] !== data["publicKey"]["owner"] do
Logger.error(
"Owner of the public key is not the same as the actor - not saving the public key."
)
nil
else
maybe_make_public_key_object(data)
end
shared_inbox =
@ -1624,7 +1642,7 @@ defp object_to_user_data(data, additional) do
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: also_known_as,
public_key: public_key,
signing_key: signing_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
pinned_objects: pinned_objects,

View file

@ -60,7 +60,26 @@ defp relay_active?(conn, _) do
end
end
def user(conn, %{"nickname" => nickname}) do
@doc """
Render the user's AP data
WARNING: we cannot actually check if the request has a fragment! so let's play defensively
- IF we have a valid signature, serve full user
- IF we do not, and authorized_fetch_mode is enabled, serve the key only
- OTHERWISE, serve the full actor (since we don't need to worry about the signature)
"""
def user(%{assigns: %{valid_signature: true}} = conn, params) do
render_full_user(conn, params)
end
def user(conn, params) do
if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
render_key_only_user(conn, params)
else
render_full_user(conn, params)
end
end
defp render_full_user(conn, %{"nickname" => nickname}) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
@ -72,6 +91,18 @@ def user(conn, %{"nickname" => nickname}) do
end
end
def render_key_only_user(conn, %{"nickname" => nickname}) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("keys.json", %{user: user})
else
nil -> {:error, :not_found}
%{local: false} -> {:error, :not_found}
end
end
def object(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),

View file

@ -14,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
alias Pleroma.Object.Containment
alias Pleroma.Signature
require Pleroma.Constants
@ -23,8 +22,7 @@ def validate(object, meta)
def validate(%{"type" => type, "id" => _id} = data, meta)
when type in Pleroma.Constants.actor_types() do
with :ok <- validate_pubkey(data),
:ok <- validate_inbox(data),
with :ok <- validate_inbox(data),
:ok <- contain_collection_origin(data) do
{:ok, data, meta}
else
@ -35,33 +33,6 @@ def validate(%{"type" => type, "id" => _id} = data, meta)
def validate(_, _), do: {:error, "Not a user object"}
defp mabye_validate_owner(nil, _actor), do: :ok
defp mabye_validate_owner(actor, actor), do: :ok
defp mabye_validate_owner(_owner, _actor), do: :error
defp validate_pubkey(
%{"id" => id, "publicKey" => %{"id" => pk_id, "publicKeyPem" => _key}} = data
)
when id != nil do
with {_, {:ok, kactor}} <- {:key, Signature.key_id_to_actor_id(pk_id)},
true <- id == kactor,
:ok <- mabye_validate_owner(Map.get(data, "owner"), id) do
:ok
else
{:key, _} ->
{:error, "Unable to determine actor id from key id"}
false ->
{:error, "Key id does not relate to user id"}
_ ->
{:error, "Actor does not own its public key"}
end
end
# pubkey is optional atm
defp validate_pubkey(_data), do: :ok
defp validate_inbox(%{"id" => id, "inbox" => inbox}) do
case Containment.same_origin(id, inbox) do
:ok -> :ok

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@ def render("show.json", %{object: object, multiple: multiple, options: options}
expired: expired,
multiple: multiple,
votes_count: votes_count,
voters_count: voters_count(object),
voters_count: voters_count(multiple, object),
options: options,
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
}
@ -68,11 +68,19 @@ defp options_and_votes_count(options) do
end)
end
defp voters_count(%{data: %{"voters" => voters}}) when is_list(voters) do
defp voters_count(false, _poll_data) do
# Mastodon always sets voter count to "null" unless multiple options were selectable
# Some clients may rely on this to detect multiple selection polls and it can mess
# up percentages for some clients if we never got a correct remote voter count and
# only count local voters here; see https://akkoma.dev/AkkomaGang/akkoma/issues/190
nil
end
defp voters_count(_multiple, %{data: %{"voters" => voters}}) when is_list(voters) do
length(voters)
end
defp voters_count(_), do: 0
defp voters_count(_, _), do: 0
defp voted_and_own_votes(%{object: object} = params, options) do
if params[:for] do

View file

@ -0,0 +1,32 @@
defmodule Pleroma.Web.Plugs.EnsureUserPublicKeyPlug do
@moduledoc """
This plug will attempt to pull in a user's public key if we do not have it.
We _should_ be able to request the URL from the key URL...
"""
alias Pleroma.User
def init(options), do: options
def call(conn, _opts) do
key_id = key_id_from_conn(conn)
unless is_nil(key_id) do
User.SigningKey.get_or_fetch_by_key_id(key_id)
# now we SHOULD have the user that owns the key locally. maybe.
# if we don't, we'll error out when we try to validate.
end
conn
end
defp key_id_from_conn(conn) do
case HTTPSignatures.signature_for_conn(conn) do
%{"keyId" => key_id} when is_binary(key_id) ->
key_id
_ ->
nil
end
end
end

View file

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

View file

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

View file

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

View file

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

View file

@ -7,51 +7,51 @@
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]},
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "6630c42aaaab124e697b4e513190c89d8b64e410", [ref: "6630c42aaaab124e697b4e513190c89d8b64e410"]},
"castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
"comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"},
"concurrent_limiter": {:git, "https://akkoma.dev/AkkomaGang/concurrent-limiter.git", "a9e0b3d64574bdba761f429bb4fba0cf687b3338", [ref: "a9e0b3d64574bdba761f429bb4fba0cf687b3338"]},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
"credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"},
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
"eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"},
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.0", "440719cd74f09b3f01c84455707a2c3972b725c513808e68eb6c5b0ab82bf523", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 0.18.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "f1512812dc196bcb932a96c82e55f69b543dc125e9d39f5e3631a9c4ec65ef12"},
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.2", "79350a53246ac5ec27326d208496aebceb77fa82a91744f66a9154560f0759d3", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0 and < 0.20.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "6149c1c4a5ba6602a76cb09ee7a269eb60dab9694a1dbbb797f032555212de75"},
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
"elasticsearch": {:git, "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", "6cd946f75f6ab9042521a009d1d32d29a90113ca", [ref: "main"]},
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
"elixir_xml_to_map": {:hex, :elixir_xml_to_map, "3.1.0", "4d6260486a8cce59e4bf3575fe2dd2a24766546ceeef9f93fcec6f7c62a2827a", [:mix], [{:erlsom, "~> 1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "8fe5f2e75f90bab07ee2161120c2dc038ebcae8135554f5582990f1c8c21f911"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"ex_aws": {:hex, :ex_aws, "2.5.4", "86c5bb870a49e0ab6f5aa5dd58cf505f09d2624ebe17530db3c1b61c88a673af", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82bd0091bb9a5bb190139599f922ff3fc7aebcca4374d65c99c4e23aa6d1625"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"},
"ex_aws": {:hex, :ex_aws, "2.5.6", "6f642e0f82eff10a9b470044f084b81a791cf15b393d647ea5f3e65da2794e3d", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c69eec59e31fdd89d0beeb1d97e16518dd1b23ad95b3d5c9f1dcfec23d97f960"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.4", "87aaf4a2f24a48f516d7f5aaced9d128dd5d0f655c4431f9037a11a85c71109c", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "c06e7f68b33f7c0acba1361dbd951c79661a28f85aa2e0582990fccca4425355"},
"ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"},
"ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
"ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"},
"ex_syslogger": {:hex, :ex_syslogger, "2.0.0", "de6de5c5472a9c4fdafb28fa6610e381ae79ebc17da6490b81d785d68bd124c9", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "a52b2fe71764e9e6ecd149ab66635812f68e39279cbeee27c52c0e35e8b8019e"},
"excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"},
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
"fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"},
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
"file_ex": {:git, "https://akkoma.dev/AkkomaGang/file_ex.git", "cc7067c7d446c2526e9ecf91d40896b088851569", [ref: "cc7067c7d446c2526e9ecf91d40896b088851569"]},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"},
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
"gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
@ -61,33 +61,34 @@
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"joken": {:hex, :joken, "2.6.1", "2ca3d8d7f83bf7196296a3d9b2ecda421a404634bfc618159981a960020480a1", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ab26122c400b3d254ce7d86ed066d6afad27e70416df947cdcb01e13a7382e68"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
"mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
"mail": {:hex, :mail, "0.4.2", "959229676ef11dfdfb1269ce4a611622cb70415a1d6f925d79e547848bafc14d", [:mix], [], "hexpm", "08e5b70c72b8d1605cb88ef2df2c7e41d002210a621503ea1c13f1a7916b6bd3"},
"majic": {:git, "https://akkoma.dev/AkkomaGang/majic.git", "80540b36939ec83f48e76c61e5000e0fd67706f0", [ref: "80540b36939ec83f48e76c61e5000e0fd67706f0"]},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "b21ab7754024af096f2d14247574f55f0063295b", [ref: "b21ab7754024af096f2d14247574f55f0063295b"]},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
"mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"},
"mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"},
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"oban": {:hex, :oban, "2.17.10", "c3e5bd739b5c3fdc38eba1d43ab270a8c6ca4463bb779b7705c69400b0d87678", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4afd027b8e2bc3c399b54318b4f46ee8c40251fb55a285cb4e38b5363f0ee7c4"},
"open_api_spex": {:hex, :open_api_spex, "3.19.1", "65ccb5d06e3d664d1eec7c5ea2af2289bd2f37897094a74d7219fb03fc2b5994", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "392895827ce2984a3459c91a484e70708132d8c2c6c5363972b4b91d6bbac3dd"},
"oban": {:hex, :oban, "2.17.12", "33fb0cbfb92b910d48dd91a908590fe3698bb85eacec8cd0d9bc6aa13dddd6d6", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7a647d6cd6bb300073db17faabce22d80ae135da3baf3180a064fa7c4fa046e3"},
"open_api_spex": {:hex, :open_api_spex, "3.21.2", "6a704f3777761feeb5657340250d6d7332c545755116ca98f33d4b875777e1e5", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f42ae6ed668b895ebba3e02773cfb4b41050df26f803f2ef634c72a7687dc387"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"},
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
@ -95,8 +96,8 @@
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
"plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
"poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
@ -104,7 +105,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"},
"pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"},
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
"remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
"search_parser": {:git, "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git", "08971a81e68686f9ac465cfb6661d51c5e4e1e7f", [ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"]},
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
@ -114,22 +115,22 @@
"swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
"telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"},
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
"tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"},
"tesla": {:hex, :tesla, "1.13.0", "24a068a48d107080dd7c943a593997eee265977a38020eb2ab657cca78a12502", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7b8fc8f6b0640fa0d090af7889d12eb396460e044b6f8688a8e55e30406a2200"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
"ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
"vex": {:hex, :vex, "0.9.2", "fe061acc9e0907d983d46b51bf35d58176f0fe6eb7ba3b33c9336401bf42b6d1", [:mix], [], "hexpm", "76e709a9762e98c6b462dfce92e9b5dfbf712839227f2da8add6dd11549b12cb"},
"web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
"websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
}

View file

@ -0,0 +1,13 @@
defmodule Pleroma.Repo.Migrations.CreateSigningKeyTable do
use Ecto.Migration
def change do
create table(:signing_keys, primary_key: false) do
add :user_id, references(:users, type: :uuid, on_delete: :delete_all)
add :key_id, :text, primary_key: true
add :public_key, :text
add :private_key, :text
timestamps()
end
end
end

View file

@ -0,0 +1,37 @@
defmodule Pleroma.Repo.Migrations.MoveSigningKeys do
use Ecto.Migration
alias Pleroma.User
alias Pleroma.Repo
import Ecto.Query
def up do
# we do not handle remote users here!
# because we want to store a key id -> user id mapping, and we don't
# currently store key ids for remote users...
query =
from(u in User)
|> where(local: true)
Repo.stream(query, timeout: :infinity)
|> Enum.each(fn
%User{id: user_id, keys: private_key, local: true, ap_id: ap_id} ->
IO.puts("Migrating user #{user_id}")
# we can precompute the public key here...
# we do use it on every user view which makes it a bit of a dos attack vector
# so we should probably cache it
{:ok, public_key} = User.SigningKey.private_pem_to_public_pem(private_key)
key = %User.SigningKey{
user_id: user_id,
public_key: public_key,
key_id: User.SigningKey.local_key_id(ap_id),
private_key: private_key
}
{:ok, _} = Repo.insert(key)
end)
end
# no need to rollback
def down, do: :ok
end

View file

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

View file

@ -353,7 +353,7 @@ test "with the --keep-threads option it keeps old threads with bookmarked posts"
test "We don't have unexpected tables which may contain objects that are referenced by activities" do
# We can delete orphaned activities. For that we look for the objects they reference in the 'objects', 'activities', and 'users' table.
# If someone adds another table with objects (idk, maybe with separate relations, or collections or w/e), then we need to make sure we
# If someone adds another table with objects (idk, maybe with separate relations, or collections or w/e), then we need to make sure we
# add logic for that in the 'prune_objects' task so that we don't wrongly delete their corresponding activities.
# So when someone adds (or removes) a table, this test will fail.
# Either the table contains objects which can be referenced from the activities table
@ -401,6 +401,7 @@ test "We don't have unexpected tables which may contain objects that are referen
["rich_media_card"],
["scheduled_activities"],
["schema_migrations"],
["signing_keys"],
["thread_mutes"],
["user_follows_hashtag"],
["user_frontend_setting_profiles"],

View file

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

View file

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

@ -203,7 +203,6 @@ defp spoofed_object_with_ids(
:ok
end
@tag capture_log: true
test "it works when fetching the OP actor errors out" do
# Here we simulate a case where the author of the OP can't be read
assert {:ok, _} =
@ -779,7 +778,7 @@ test "should return ok if the content type is application/activity+json" do
assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
end
test "should return ok if the content type is application/ld+json with a profile" do
test "should return ok if the content type is application/ld+json with the ActivityStream profile" do
Tesla.Mock.mock(fn
%{
method: :get,
@ -799,6 +798,26 @@ test "should return ok if the content type is application/ld+json with a profile
assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
end
test "should return ok if the content type is application/ld+json with several profiles" do
Tesla.Mock.mock(fn
%{
method: :get,
url: "https://mastodon.social/2"
} ->
%Tesla.Env{
status: 200,
url: "https://mastodon.social/2",
headers: [
{"content-type",
"application/ld+json; profile=\"https://example.org/ns/superduperspec https://www.w3.org/ns/activitystreams\""}
],
body: "{}"
}
end)
assert {:ok, _, "{}"} = Fetcher.get_object("https://mastodon.social/2")
end
test "should not return ok with other content types" do
Tesla.Mock.mock(fn
%{

View file

@ -6,7 +6,6 @@ defmodule Pleroma.SignatureTest do
use Pleroma.DataCase, async: false
@moduletag :mocked
import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
import Mock
@ -35,25 +34,23 @@ defp make_fake_conn(key_id),
do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}}
describe "fetch_public_key/1" do
test "it returns key" do
test "it returns the key" do
expected_result = {:ok, @rsa_public_key}
user = insert(:user, public_key: @public_key)
user =
insert(:user)
|> with_signing_key(public_key: @public_key)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
end
test "it returns error when not found user" do
assert capture_log(fn ->
assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) ==
{:error, :error}
end) =~ "[error] Could not decode user"
end
test "it returns error if public key is nil" do
user = insert(:user, public_key: nil)
# this actually needs the URL to be valid
user = insert(:user)
key_id = user.ap_id <> "#main-key"
Tesla.Mock.mock(fn %{url: ^key_id} -> {:ok, %{status: 404}} end)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error}
assert {:error, _} = Signature.fetch_public_key(make_fake_conn(user.ap_id))
end
end
@ -63,12 +60,6 @@ test "it returns key" do
assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key}
end
test "it returns error when not found user" do
assert capture_log(fn ->
{:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id"))
end) =~ "[error] Could not decode user"
end
end
defp split_signature(sig) do
@ -104,9 +95,9 @@ defp assert_part_equal(part_a, part_b) do
test "it returns signature headers" do
user =
insert(:user, %{
ap_id: "https://mastodon.social/users/lambadalambda",
keys: @private_key
ap_id: "https://mastodon.social/users/lambadalambda"
})
|> with_signing_key(private_key: @private_key)
headers = %{
host: "test.test",
@ -121,50 +112,15 @@ test "it returns signature headers" do
"keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\""
)
end
test "it returns error" do
user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""})
assert Signature.sign(
user,
%{host: "test.test", "content-length": "100"}
) == {:error, []}
end
end
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
{:ok, "https://example.com/users/1234"}
end
test "it reverses the key id to actor id" do
user =
insert(:user)
|> with_signing_key()
test "it properly deduces the actor id for mastodon and pleroma" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") ==
{:ok, "https://example.com/users/1234"}
end
test "it deduces the actor id for gotoSocial" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/main-key") ==
{:ok, "https://example.com/users/1234"}
end
test "it deduces the actor ID for streams" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234?operation=getkey") ==
{:ok, "https://example.com/users/1234"}
end
test "it deduces the actor ID for bridgy" do
assert Signature.key_id_to_actor_id("https://example.com/1234#key") ==
{:ok, "https://example.com/1234"}
end
test "it calls webfinger for 'acct:' accounts" do
with_mock(Pleroma.Web.WebFinger,
finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end
) do
assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") ==
{:ok, "https://gensokyo.2hu/users/raymoo"}
end
assert Signature.key_id_to_actor_id(user.signing_key.key_id) == {:ok, user.ap_id}
end
end

View file

@ -115,6 +115,15 @@ test "verify webp files are skipped" do
assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop}
end
test "verify bmp files are skipped" do
upload = %Pleroma.Upload{
name: "sample.bmp",
content_type: "image/bmp"
}
assert Filter.Exiftool.StripMetadata.filter(upload) == {:ok, :noop}
end
test "verify svg files are skipped" do
upload = %Pleroma.Upload{
name: "sample.svg",

View file

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

View file

@ -639,11 +639,12 @@ test "it sets the password_hash, ap_id, private key and followers collection add
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
assert is_binary(changeset.changes[:password_hash])
assert is_binary(changeset.changes[:keys])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
assert is_binary(changeset.changes[:keys])
assert changeset.changes[:signing_key]
assert changeset.changes[:signing_key].valid?
assert is_binary(changeset.changes[:signing_key].changes.private_key)
assert is_binary(changeset.changes[:signing_key].changes.public_key)
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end
@ -814,7 +815,6 @@ test "gets an existing user by fully qualified nickname, case insensitive" do
assert user == fetched_user
end
@tag capture_log: true
test "returns nil if no user could be fetched" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
assert fetched_user == "not found nonexistant@social.heldscal.la"
@ -871,7 +871,6 @@ test "if nicknames clash, the old user gets a prefix with the old id to the nick
assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org"
end
@tag capture_log: true
test "it returns the old user if stale, but unfetchable" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
@ -1665,7 +1664,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
name: "qqqqqqq",
password_hash: "pdfk2$1b3n159001",
keys: "RSA begin buplic key",
public_key: "--PRIVATE KEYE--",
avatar: %{"a" => "b"},
tags: ["qqqqq"],
banner: %{"a" => "b"},
@ -1704,8 +1702,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do
email: nil,
name: nil,
password_hash: nil,
keys: "RSA begin buplic key",
public_key: "--PRIVATE KEYE--",
avatar: %{},
tags: [],
last_refreshed_at: nil,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -57,7 +57,6 @@ test "it ignores an incoming notice if we already have it" do
assert activity == returned_activity
end
@tag capture_log: true
test "it fetches reply-to activities if we don't have them" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
@ -537,7 +536,6 @@ test "returns object with inReplyTo when denied incoming reply", %{data: data} d
assert modified_object["inReplyTo"] == []
end
@tag capture_log: true
test "returns modified object when allowed incoming reply", %{data: data} do
object_with_reply =
Map.put(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -47,7 +47,6 @@ def instance_factory(attrs \\ %{}) do
end
def user_factory(attrs \\ %{}) do
pem = Enum.random(@rsa_keys)
# Argon2.hash_pwd_salt("test")
# it really eats CPU time, so we use a precomputed hash
password_hash =
@ -64,8 +63,7 @@ def user_factory(attrs \\ %{}) do
last_refreshed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{},
multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
ap_enabled: true,
keys: pem
ap_enabled: true
}
urls =
@ -97,6 +95,28 @@ def user_factory(attrs \\ %{}) do
|> merge_attributes(attrs)
end
def with_signing_key(%User{} = user, attrs \\ %{}) do
signing_key =
build(:signing_key, %{user: user, key_id: "#{user.ap_id}#main-key"})
|> merge_attributes(attrs)
insert(signing_key)
%{user | signing_key: signing_key}
end
def signing_key_factory(attrs \\ %{}) do
pem = Enum.random(@rsa_keys)
user = attrs[:user] || insert(:user)
{:ok, public_key} = Pleroma.User.SigningKey.private_pem_to_public_pem(pem)
%Pleroma.User.SigningKey{
user_id: user.id,
public_key: attrs[:public_key] || public_key,
private_key: attrs[:private_key] || pem,
key_id: attrs[:key_id]
}
end
def user_relationship_factory(attrs \\ %{}) do
source = attrs[:source] || insert(:user)
target = attrs[:target] || insert(:user)

View file

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

View file

@ -424,6 +424,15 @@ def get("http://mastodon.example.org/users/admin", _, _, _) do
}}
end
def get("http://mastodon.example.org/users/admin/main-key", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json"),
headers: activitypub_object_headers()
}}
end
def get(
"http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true",
_,
@ -958,6 +967,15 @@ def get("https://mastodon.social/users/lambadalambda", _, _, _) do
}}
end
def get("https://mastodon.social/users/lambadalambda#main-key", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/lambadalambda.json"),
headers: activitypub_object_headers()
}}
end
def get("https://mastodon.social/users/lambadalambda/collections/featured", _, _, _) do
{:ok,
%Tesla.Env{
@ -1403,6 +1421,15 @@ def get("https://relay.mastodon.host/actor", _, _, _) do
}}
end
def get("https://relay.mastodon.host/actor#main-key", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/relay/relay.json"),
headers: activitypub_object_headers()
}}
end
def get("http://localhost:4001/", _, "", [{"accept", "text/html"}]) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}}
end

View file

@ -0,0 +1,10 @@
defmodule Pleroma.Test.MatchingHelpers do
import ExUnit.Assertions
@assoc_fields [
:signing_key
]
def assert_user_match(actor1, actor2) do
assert Ecto.reset_fields(actor1, @assoc_fields) == Ecto.reset_fields(actor2, @assoc_fields)
end
end

View file

@ -3,7 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
ExUnit.start(exclude: [:federated, :erratic] ++ os_exclude)
ExUnit.start(
capture_log: true,
exclude: [:federated, :erratic] ++ os_exclude
)
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)