> = filename
+ ) do
+ Path.join([a, b, c, filename])
+ end
end
diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex
index d1e166022..7c7431db6 100644
--- a/lib/pleroma/upload/filter/mogrify.ex
+++ b/lib/pleroma/upload/filter/mogrify.ex
@@ -8,9 +8,16 @@ defmodule Pleroma.Upload.Filter.Mogrify do
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
@type conversions :: conversion() | [conversion()]
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+ @mogrify_impl Application.compile_env(
+ :pleroma,
+ [__MODULE__, :mogrify_impl],
+ Pleroma.MogrifyWrapper
+ )
+
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
try do
- do_filter(file, Pleroma.Config.get!([__MODULE__, :args]))
+ do_filter(file, @config_impl.get!([__MODULE__, :args]))
{:ok, :filtered}
rescue
e in ErlangError ->
@@ -22,9 +29,9 @@ defmodule Pleroma.Upload.Filter.Mogrify do
def do_filter(file, filters) do
file
- |> Mogrify.open()
+ |> @mogrify_impl.open()
|> mogrify_filter(filters)
- |> Mogrify.save(in_place: true)
+ |> @mogrify_impl.save(in_place: true)
end
defp mogrify_filter(mogrify, nil), do: mogrify
@@ -38,10 +45,10 @@ defmodule Pleroma.Upload.Filter.Mogrify do
defp mogrify_filter(mogrify, []), do: mogrify
defp mogrify_filter(mogrify, {action, options}) do
- Mogrify.custom(mogrify, action, options)
+ @mogrify_impl.custom(mogrify, action, options)
end
defp mogrify_filter(mogrify, action) when is_binary(action) do
- Mogrify.custom(mogrify, action)
+ @mogrify_impl.custom(mogrify, action)
end
end
diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex
index e4a309cea..7aab05b36 100644
--- a/lib/pleroma/uploaders/local.ex
+++ b/lib/pleroma/uploaders/local.ex
@@ -19,7 +19,7 @@ defmodule Pleroma.Uploaders.Local do
[file | folders] ->
path = Path.join([upload_path()] ++ Enum.reverse(folders))
- File.mkdir_p!(path)
+ Pleroma.Backports.mkdir_p!(path)
{path, file}
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 517009253..84551afd5 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -19,6 +19,7 @@ defmodule Pleroma.User do
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
+ alias Pleroma.Hashtag
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA
@@ -27,6 +28,7 @@ defmodule Pleroma.User do
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.User.HashtagFollow
alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
@@ -148,7 +150,7 @@ defmodule Pleroma.User do
field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
- field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
+ field(:also_known_as, {:array, ObjectValidators.BareUri}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
field(:accepts_chat_messages, :boolean, default: nil)
@@ -174,6 +176,12 @@ defmodule Pleroma.User do
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
+ many_to_many(:followed_hashtags, Hashtag,
+ on_replace: :delete,
+ on_delete: :delete_all,
+ join_through: HashtagFollow
+ )
+
for {relationship_type,
[
{outgoing_relation, outgoing_relation_target},
@@ -300,7 +308,7 @@ defmodule Pleroma.User do
def binary_id(%User{} = user), do: binary_id(user.id)
- @doc "Returns status account"
+ @doc "Returns account status"
@spec account_status(User.t()) :: account_status()
def account_status(%User{is_active: false}), do: :deactivated
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
@@ -419,6 +427,11 @@ defmodule Pleroma.User do
end
end
+ def image_description(image, default \\ "")
+
+ def image_description(%{"name" => name}, _default), do: name
+ def image_description(_, default), do: default
+
# Should probably be renamed or removed
@spec ap_id(User.t()) :: String.t()
def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
@@ -882,7 +895,7 @@ defmodule Pleroma.User do
end)
end
- def validate_email_not_in_blacklisted_domain(changeset, field) do
+ defp validate_email_not_in_blacklisted_domain(changeset, field) do
validate_change(changeset, field, fn _, value ->
valid? =
Config.get([User, :email_blacklist])
@@ -899,9 +912,9 @@ defmodule Pleroma.User do
end)
end
- def maybe_validate_required_email(changeset, true), do: changeset
+ defp maybe_validate_required_email(changeset, true), do: changeset
- def maybe_validate_required_email(changeset, _) do
+ defp maybe_validate_required_email(changeset, _) do
if Config.get([:instance, :account_activation_required]) do
validate_required(changeset, [:email])
else
@@ -1096,15 +1109,15 @@ defmodule Pleroma.User do
defp maybe_send_registration_email(_), do: {:ok, :noop}
- def needs_update?(%User{local: true}), do: false
+ defp needs_update?(%User{local: true}), do: false
- def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
+ defp needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
- def needs_update?(%User{local: false} = user) do
+ defp needs_update?(%User{local: false} = user) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
end
- def needs_update?(_), do: true
+ defp needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) ::
{:ok, User.t(), User.t()} | {:error, String.t()}
@@ -1695,7 +1708,9 @@ defmodule Pleroma.User do
end
end
- def block(%User{} = blocker, %User{} = blocked) do
+ def block(blocker, blocked, params \\ %{})
+
+ def block(%User{} = blocker, %User{} = blocked, params) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
if following?(blocker, blocked) do
@@ -1725,12 +1740,33 @@ defmodule Pleroma.User do
{:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
- add_to_block(blocker, blocked)
+
+ duration = Map.get(params, :duration, 0)
+
+ expires_at =
+ if duration > 0 do
+ DateTime.utc_now()
+ |> DateTime.add(duration)
+ else
+ nil
+ end
+
+ user_block = add_to_block(blocker, blocked, expires_at)
+
+ if duration > 0 do
+ Pleroma.Workers.MuteExpireWorker.new(
+ %{"op" => "unblock_user", "blocker_id" => blocker.id, "blocked_id" => blocked.id},
+ scheduled_at: expires_at
+ )
+ |> Oban.insert()
+ end
+
+ user_block
end
# helper to handle the block given only an actor's AP id
- def block(%User{} = blocker, %{ap_id: ap_id}) do
- block(blocker, get_cached_by_ap_id(ap_id))
+ def block(%User{} = blocker, %{ap_id: ap_id}, params) do
+ block(blocker, get_cached_by_ap_id(ap_id), params)
end
def unblock(%User{} = blocker, %User{} = blocked) do
@@ -1971,7 +2007,7 @@ defmodule Pleroma.User do
end
@spec purge_user_changeset(User.t()) :: Ecto.Changeset.t()
- def purge_user_changeset(user) do
+ defp purge_user_changeset(user) do
# "Right to be forgotten"
# https://gdpr.eu/right-to-be-forgotten/
change(user, %{
@@ -2143,7 +2179,7 @@ defmodule Pleroma.User do
Repo.all(query)
end
- def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
+ defp delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
Notification
|> join(:inner, [n], activity in assoc(n, :activity))
|> where([n, a], fragment("? = ?", a.actor, ^ap_id))
@@ -2602,7 +2638,7 @@ defmodule Pleroma.User do
end
end
- # Internal function; public one is `deactivate/2`
+ # Internal function; public one is `set_activation/2`
defp set_activation_status(user, status) do
user
|> cast(%{is_active: status}, [:is_active])
@@ -2621,7 +2657,7 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- def validate_fields(changeset, remote? \\ false) do
+ defp validate_fields(changeset, remote?) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Config.get([:instance, limit_name], 0)
@@ -2766,10 +2802,10 @@ defmodule Pleroma.User do
set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
end
- @spec add_to_block(User.t(), User.t()) ::
+ @spec add_to_block(User.t(), User.t(), integer() | nil) ::
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
- defp add_to_block(%User{} = user, %User{} = blocked) do
- with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
+ defp add_to_block(%User{} = user, %User{} = blocked, expires_at) do
+ with {:ok, relationship} <- UserRelationship.create_block(user, blocked, expires_at) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship}
end
@@ -2856,4 +2892,54 @@ defmodule Pleroma.User do
birthday_month: month
})
end
+
+ defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user)
+ when is_list(follows),
+ do: user
+
+ defp maybe_load_followed_hashtags(%User{} = user) do
+ followed_hashtags = HashtagFollow.get_by_user(user)
+ %{user | followed_hashtags: followed_hashtags}
+ end
+
+ def followed_hashtags(%User{followed_hashtags: follows})
+ when is_list(follows),
+ do: follows
+
+ def followed_hashtags(%User{} = user) do
+ {:ok, user} =
+ user
+ |> maybe_load_followed_hashtags()
+ |> set_cache()
+
+ user.followed_hashtags
+ end
+
+ def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
+ Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}")
+ user = maybe_load_followed_hashtags(user)
+
+ with {:ok, _} <- HashtagFollow.new(user, hashtag),
+ follows <- HashtagFollow.get_by_user(user),
+ %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
+ user
+ |> set_cache()
+ end
+ end
+
+ def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
+ Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}")
+ user = maybe_load_followed_hashtags(user)
+
+ with {:ok, _} <- HashtagFollow.delete(user, hashtag),
+ follows <- HashtagFollow.get_by_user(user),
+ %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
+ user
+ |> set_cache()
+ end
+ end
+
+ def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
+ not is_nil(HashtagFollow.get(user, hashtag))
+ end
end
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
index d77d49890..3f67cdf0c 100644
--- a/lib/pleroma/user/backup.ex
+++ b/lib/pleroma/user/backup.ex
@@ -16,6 +16,7 @@ defmodule Pleroma.User.Backup do
alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.Repo
+ alias Pleroma.SafeZip
alias Pleroma.Uploaders.Uploader
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -179,12 +180,12 @@ defmodule Pleroma.User.Backup do
end
@files [
- ~c"actor.json",
- ~c"outbox.json",
- ~c"likes.json",
- ~c"bookmarks.json",
- ~c"followers.json",
- ~c"following.json"
+ "actor.json",
+ "outbox.json",
+ "likes.json",
+ "bookmarks.json",
+ "followers.json",
+ "following.json"
]
@spec run(t()) :: {:ok, t()} | {:error, :failed}
@@ -192,7 +193,7 @@ defmodule Pleroma.User.Backup do
backup = Repo.preload(backup, :user)
tempfile = Path.join([backup.tempdir, backup.file_name])
- with {_, :ok} <- {:mkdir, File.mkdir_p(backup.tempdir)},
+ with {_, :ok} <- {:mkdir, Pleroma.Backports.mkdir_p(backup.tempdir)},
{_, :ok} <- {:actor, actor(backup.tempdir, backup.user)},
{_, :ok} <- {:statuses, statuses(backup.tempdir, backup.user)},
{_, :ok} <- {:likes, likes(backup.tempdir, backup.user)},
@@ -200,7 +201,7 @@ defmodule Pleroma.User.Backup do
{_, :ok} <- {:followers, followers(backup.tempdir, backup.user)},
{_, :ok} <- {:following, following(backup.tempdir, backup.user)},
{_, {:ok, _zip_path}} <-
- {:zip, :zip.create(to_charlist(tempfile), @files, cwd: to_charlist(backup.tempdir))},
+ {:zip, SafeZip.zip(tempfile, @files, backup.tempdir)},
{_, {:ok, %File.Stat{size: zip_size}}} <- {:filestat, File.stat(tempfile)},
{:ok, updated_backup} <- update_record(backup, %{file_size: zip_size}) do
{:ok, updated_backup}
@@ -246,7 +247,13 @@ defmodule Pleroma.User.Backup do
defp actor(dir, user) do
with {:ok, json} <-
UserView.render("user.json", %{user: user})
- |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"})
+ |> Map.merge(%{
+ "bookmarks" => "bookmarks.json",
+ "likes" => "likes.json",
+ "outbox" => "outbox.json",
+ "followers" => "followers.json",
+ "following" => "following.json"
+ })
|> Jason.encode() do
File.write(Path.join(dir, "actor.json"), json)
end
diff --git a/lib/pleroma/user/hashtag_follow.ex b/lib/pleroma/user/hashtag_follow.ex
new file mode 100644
index 000000000..3e28b130b
--- /dev/null
+++ b/lib/pleroma/user/hashtag_follow.ex
@@ -0,0 +1,55 @@
+defmodule Pleroma.User.HashtagFollow do
+ use Ecto.Schema
+ import Ecto.Query
+ import Ecto.Changeset
+
+ alias Pleroma.Hashtag
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ schema "user_follows_hashtag" do
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+ belongs_to(:hashtag, Hashtag)
+ end
+
+ def changeset(%__MODULE__{} = user_hashtag_follow, attrs) do
+ user_hashtag_follow
+ |> cast(attrs, [:user_id, :hashtag_id])
+ |> unique_constraint(:hashtag_id,
+ name: :user_hashtag_follows_user_id_hashtag_id_index,
+ message: "already following"
+ )
+ |> validate_required([:user_id, :hashtag_id])
+ end
+
+ def new(%User{} = user, %Hashtag{} = hashtag) do
+ %__MODULE__{}
+ |> changeset(%{user_id: user.id, hashtag_id: hashtag.id})
+ |> Repo.insert(on_conflict: :nothing)
+ end
+
+ def delete(%User{} = user, %Hashtag{} = hashtag) do
+ with %__MODULE__{} = user_hashtag_follow <- get(user, hashtag) do
+ Repo.delete(user_hashtag_follow)
+ else
+ _ -> {:ok, nil}
+ end
+ end
+
+ def get(%User{} = user, %Hashtag{} = hashtag) do
+ from(hf in __MODULE__)
+ |> where([hf], hf.user_id == ^user.id and hf.hashtag_id == ^hashtag.id)
+ |> Repo.one()
+ end
+
+ def get_by_user(%User{} = user) do
+ user
+ |> followed_hashtags_query()
+ |> Repo.all()
+ end
+
+ def followed_hashtags_query(%User{} = user) do
+ Ecto.assoc(user, :followed_hashtags)
+ |> Ecto.Query.order_by([h], desc: h.id)
+ end
+end
diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex
index 82fcc1cdd..07b6e46f7 100644
--- a/lib/pleroma/user_relationship.ex
+++ b/lib/pleroma/user_relationship.ex
@@ -55,9 +55,13 @@ defmodule Pleroma.UserRelationship do
def user_relationship_mappings, do: Pleroma.UserRelationship.Type.__enum_map__()
+ def datetime_impl do
+ Application.get_env(:pleroma, :datetime_impl, Pleroma.DateTime.Impl)
+ end
+
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship
- |> cast(params, [:relationship_type, :source_id, :target_id, :expires_at])
+ |> cast(params, [:relationship_type, :source_id, :target_id, :expires_at, :inserted_at])
|> validate_required([:relationship_type, :source_id, :target_id])
|> unique_constraint(:relationship_type,
name: :user_relationships_source_id_relationship_type_target_id_index
@@ -65,6 +69,7 @@ defmodule Pleroma.UserRelationship do
|> validate_not_self_relationship()
end
+ @spec exists?(any(), Pleroma.User.t(), Pleroma.User.t()) :: boolean()
def exists?(relationship_type, %User{} = source, %User{} = target) do
UserRelationship
|> where(relationship_type: ^relationship_type, source_id: ^source.id, target_id: ^target.id)
@@ -90,7 +95,8 @@ defmodule Pleroma.UserRelationship do
relationship_type: relationship_type,
source_id: source.id,
target_id: target.id,
- expires_at: expires_at
+ expires_at: expires_at,
+ inserted_at: datetime_impl().utc_now()
})
|> Repo.insert(
on_conflict: {:replace_all_except, [:id, :inserted_at]},
@@ -187,7 +193,8 @@ defmodule Pleroma.UserRelationship do
{[:mute], []}
nil ->
- {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]}
+ {[:block, :mute, :notification_mute, :reblog_mute, :endorsement],
+ [:block, :inverse_subscription]}
unknown ->
raise "Unsupported :subset option value: #{inspect(unknown)}"
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index a2a94a0ff..62c7a7b31 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -924,6 +924,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end
+ # Essentially, either look for activities addressed to `recipients`, _OR_ ones
+ # that reference a hashtag that the user follows
+ # Firstly, two fallbacks in case there's no hashtag constraint, or the user doesn't
+ # follow any
+ defp restrict_recipients_or_hashtags(query, recipients, user, nil) do
+ restrict_recipients(query, recipients, user)
+ end
+
+ defp restrict_recipients_or_hashtags(query, recipients, user, []) do
+ restrict_recipients(query, recipients, user)
+ end
+
+ defp restrict_recipients_or_hashtags(query, recipients, _user, hashtag_ids) do
+ from([activity, object] in query)
+ |> join(:left, [activity, object], hto in "hashtags_objects",
+ on: hto.object_id == object.id,
+ as: :hto
+ )
+ |> where(
+ [activity, object, hto: hto],
+ (hto.hashtag_id in ^hashtag_ids and ^Constants.as_public() in activity.recipients) or
+ fragment("? && ?", ^recipients, activity.recipients)
+ )
+ end
+
defp restrict_local(query, %{local_only: true}) do
from(activity in query, where: activity.local == true)
end
@@ -1414,7 +1439,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_preload_report_notes(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
- |> restrict_recipients(recipients, opts[:user])
+ |> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags])
|> restrict_replies(opts)
|> restrict_since(opts)
|> restrict_local(opts)
@@ -1542,16 +1567,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp get_actor_url(_url), do: nil
- defp normalize_image(%{"url" => url}) do
+ defp normalize_image(%{"url" => url} = data) do
%{
"type" => "Image",
"url" => [%{"href" => url}]
}
+ |> maybe_put_description(data)
end
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
defp normalize_image(_), do: nil
+ defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do
+ Map.put(map, "name", description)
+ end
+
+ defp maybe_put_description(map, _), do: map
+
defp object_to_user_data(data, additional) do
fields =
data
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index a08eda5f4..daf0d38e6 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -53,7 +53,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
)
plug(:log_inbox_metadata when action in [:inbox])
- plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
defp relay_active?(conn, _) do
@@ -274,13 +273,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
- with %User{is_active: true} = recipient <- User.get_cached_by_nickname(nickname),
- {:ok, %User{is_active: true} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
+ with {:recipient_exists, %User{} = recipient} <-
+ {:recipient_exists, User.get_cached_by_nickname(nickname)},
+ {:sender_exists, {:ok, %User{} = actor}} <-
+ {:sender_exists, User.get_or_fetch_by_ap_id(params["actor"])},
+ {:recipient_active, true} <- {:recipient_active, recipient.is_active},
+ {:sender_active, true} <- {:sender_active, actor.is_active},
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
else
+ {:recipient_exists, _} ->
+ conn
+ |> put_status(:not_found)
+ |> json("User does not exist")
+
+ {:sender_exists, _} ->
+ conn
+ |> put_status(:not_found)
+ |> json("Sender does not exist")
+
+ {:recipient_active, _} ->
+ conn
+ |> put_status(:not_found)
+ |> json("User deactivated")
+
+ {:sender_active, _} ->
+ conn
+ |> put_status(:not_found)
+ |> json("Sender deactivated")
+
_ ->
conn
|> put_status(:bad_request)
@@ -482,7 +505,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> put_status(:forbidden)
|> json(message)
- {:error, message} ->
+ {:error, message} when is_binary(message) ->
conn
|> put_status(:bad_request)
|> json(message)
@@ -520,15 +543,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> json(dgettext("errors", "error"))
end
- defp set_requester_reachable(%Plug.Conn{} = conn, _) do
- with actor <- conn.params["actor"],
- true <- is_binary(actor) do
- Pleroma.Instances.set_reachable(actor)
- end
-
- conn
- end
-
defp log_inbox_metadata(%{params: %{"actor" => actor, "type" => type}} = conn, _) do
Logger.metadata(actor: actor, type: type)
conn
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 2a1e56278..046316024 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -327,8 +327,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
}, []}
end
- @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
- def block(blocker, blocked) do
+ @spec block(User.t(), User.t(), map()) :: {:ok, map(), keyword()}
+ def block(blocker, blocked, params \\ %{}) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
@@ -336,7 +336,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
"actor" => blocker.ap_id,
"object" => blocked.ap_id,
"to" => [blocked.ap_id]
- }, []}
+ }, Keyword.new(params)}
end
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index bc418d908..51ab476b7 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -108,6 +108,14 @@ defmodule Pleroma.Web.ActivityPub.MRF do
def filter(%{} = object), do: get_policies() |> filter(object)
+ def id_filter(policies, id) when is_binary(id) do
+ policies
+ |> Enum.filter(&function_exported?(&1, :id_filter, 1))
+ |> Enum.all?(& &1.id_filter(id))
+ end
+
+ def id_filter(id) when is_binary(id), do: get_policies() |> id_filter(id)
+
@impl true
def pipeline_filter(%{} = message, meta) do
object = meta[:object_data]
diff --git a/lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex b/lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex
deleted file mode 100644
index ca41c464c..000000000
--- a/lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex
+++ /dev/null
@@ -1,146 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2024 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy do
- @moduledoc """
- Dynamic activity filtering based on an RBL database
-
- This MRF makes queries to a custom DNS server which will
- respond with values indicating the classification of the domain
- the activity originated from. This method has been widely used
- in the email anti-spam industry for very fast reputation checks.
-
- e.g., if the DNS response is 127.0.0.1 or empty, the domain is OK
- Other values such as 127.0.0.2 may be used for specific classifications.
-
- Information for why the host is blocked can be stored in a corresponding TXT record.
-
- This method is fail-open so if the queries fail the activites are accepted.
-
- An example of software meant for this purpsoe is rbldnsd which can be found
- at http://www.corpit.ru/mjt/rbldnsd.html or mirrored at
- https://git.pleroma.social/feld/rbldnsd
-
- It is highly recommended that you run your own copy of rbldnsd and use an
- external mechanism to sync/share the contents of the zone file. This is
- important to keep the latency on the queries as low as possible and prevent
- your DNS server from being attacked so it fails and content is permitted.
- """
-
- @behaviour Pleroma.Web.ActivityPub.MRF.Policy
-
- alias Pleroma.Config
-
- require Logger
-
- @query_retries 1
- @query_timeout 500
-
- @impl true
- def filter(%{"actor" => actor} = activity) do
- actor_info = URI.parse(actor)
-
- with {:ok, activity} <- check_rbl(actor_info, activity) do
- {:ok, activity}
- else
- _ -> {:reject, "[DNSRBLPolicy]"}
- end
- end
-
- @impl true
- def filter(activity), do: {:ok, activity}
-
- @impl true
- def describe do
- mrf_dnsrbl =
- Config.get(:mrf_dnsrbl)
- |> Enum.into(%{})
-
- {:ok, %{mrf_dnsrbl: mrf_dnsrbl}}
- end
-
- @impl true
- def config_description do
- %{
- key: :mrf_dnsrbl,
- related_policy: "Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy",
- label: "MRF DNSRBL",
- description: "DNS RealTime Blackhole Policy",
- children: [
- %{
- key: :nameserver,
- type: {:string},
- description: "DNSRBL Nameserver to Query (IP or hostame)",
- suggestions: ["127.0.0.1"]
- },
- %{
- key: :port,
- type: {:string},
- description: "Nameserver port",
- suggestions: ["53"]
- },
- %{
- key: :zone,
- type: {:string},
- description: "Root zone for querying",
- suggestions: ["bl.pleroma.com"]
- }
- ]
- }
- end
-
- defp check_rbl(%{host: actor_host}, activity) do
- with false <- match?(^actor_host, Pleroma.Web.Endpoint.host()),
- zone when not is_nil(zone) <- Keyword.get(Config.get([:mrf_dnsrbl]), :zone) do
- query =
- Enum.join([actor_host, zone], ".")
- |> String.to_charlist()
-
- rbl_response = rblquery(query)
-
- if Enum.empty?(rbl_response) do
- {:ok, activity}
- else
- Task.start(fn ->
- reason =
- case rblquery(query, :txt) do
- [[result]] -> result
- _ -> "undefined"
- end
-
- Logger.warning(
- "DNSRBL Rejected activity from #{actor_host} for reason: #{inspect(reason)}"
- )
- end)
-
- :error
- end
- else
- _ -> {:ok, activity}
- end
- end
-
- defp get_rblhost_ip(rblhost) do
- case rblhost |> String.to_charlist() |> :inet_parse.address() do
- {:ok, _} -> rblhost |> String.to_charlist() |> :inet_parse.address()
- _ -> {:ok, rblhost |> String.to_charlist() |> :inet_res.lookup(:in, :a) |> Enum.random()}
- end
- end
-
- defp rblquery(query, type \\ :a) do
- config = Config.get([:mrf_dnsrbl])
-
- case get_rblhost_ip(config[:nameserver]) do
- {:ok, rblnsip} ->
- :inet_res.lookup(query, :in, type,
- nameservers: [{rblnsip, config[:port]}],
- timeout: @query_timeout,
- retry: @query_retries
- )
-
- _ ->
- []
- end
- end
-end
diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex
index e4fcc9935..cf07db7f3 100644
--- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex
@@ -13,6 +13,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
{:reject, activity}
end
+ @impl true
+ def id_filter(id) do
+ Logger.debug("REJECTING #{id}")
+ false
+ end
+
@impl true
def describe, do: {:ok, %{}}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex b/lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex
deleted file mode 100644
index 2cf22745a..000000000
--- a/lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex
+++ /dev/null
@@ -1,53 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2024 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ActivityPub.MRF.FODirectReply do
- @moduledoc """
- FODirectReply alters the scope of replies to activities which are Followers Only to be Direct. The purpose of this policy is to prevent broken threads for followers of the reply author because their response was to a user that they are not also following.
- """
-
- alias Pleroma.Object
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.Visibility
-
- @behaviour Pleroma.Web.ActivityPub.MRF.Policy
-
- @impl true
- def filter(
- %{
- "type" => "Create",
- "to" => to,
- "object" => %{
- "actor" => actor,
- "type" => "Note",
- "inReplyTo" => in_reply_to
- }
- } = activity
- ) do
- with true <- is_binary(in_reply_to),
- %User{follower_address: followers_collection, local: true} <- User.get_by_ap_id(actor),
- %Object{} = in_reply_to_object <- Object.get_by_ap_id(in_reply_to),
- "private" <- Visibility.get_visibility(in_reply_to_object) do
- direct_to = to -- [followers_collection]
-
- updated_activity =
- activity
- |> Map.put("cc", [])
- |> Map.put("to", direct_to)
- |> Map.put("directMessage", true)
- |> put_in(["object", "cc"], [])
- |> put_in(["object", "to"], direct_to)
-
- {:ok, updated_activity}
- else
- _ -> {:ok, activity}
- end
- end
-
- @impl true
- def filter(activity), do: {:ok, activity}
-
- @impl true
- def describe, do: {:ok, %{}}
-end
diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex
index 54ca4b735..08bcac08a 100644
--- a/lib/pleroma/web/activity_pub/mrf/policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/policy.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.Policy do
@callback filter(Pleroma.Activity.t()) :: {:ok | :reject, Pleroma.Activity.t()}
+ @callback id_filter(String.t()) :: boolean()
@callback describe() :: {:ok | :error, map()}
@callback config_description() :: %{
optional(:children) => [map()],
@@ -13,5 +14,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do
description: String.t()
}
@callback history_awareness() :: :auto | :manual
- @optional_callbacks config_description: 0, history_awareness: 0
+ @optional_callbacks config_description: 0, history_awareness: 0, id_filter: 1
end
diff --git a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex
index b07dc3b56..b3eb0b390 100644
--- a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex
+++ b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex
@@ -29,15 +29,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReply do
} = activity
) do
with true <- is_binary(in_reply_to),
- false <- match?([], cc),
+ true <- Pleroma.Constants.as_public() in to,
%User{follower_address: followers_collection, local: true} <-
User.get_by_ap_id(actor) do
updated_to =
- to
- |> Kernel.++([followers_collection])
+ [followers_collection | to]
|> Kernel.--([Pleroma.Constants.as_public()])
- updated_cc = [Pleroma.Constants.as_public()]
+ updated_cc =
+ [Pleroma.Constants.as_public() | cc]
+ |> Kernel.--([followers_collection])
updated_activity =
activity
diff --git a/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex
new file mode 100644
index 000000000..8422832dd
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex
@@ -0,0 +1,117 @@
+defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy do
+ @moduledoc "Drop remote reports if they don't contain enough information."
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+ alias Pleroma.Config
+
+ @impl true
+ def filter(%{"type" => "Flag"} = object) do
+ with {_, false} <- {:local, local?(object)},
+ {:ok, _} <- maybe_reject_all(object),
+ {:ok, _} <- maybe_reject_anonymous(object),
+ {:ok, _} <- maybe_reject_third_party(object),
+ {:ok, _} <- maybe_reject_empty_message(object) do
+ {:ok, object}
+ else
+ {:local, true} -> {:ok, object}
+ {:reject, message} -> {:reject, message}
+ end
+ end
+
+ def filter(object), do: {:ok, object}
+
+ defp maybe_reject_all(object) do
+ if Config.get([:mrf_remote_report, :reject_all]) do
+ {:reject, "[RemoteReportPolicy] Remote report"}
+ else
+ {:ok, object}
+ end
+ end
+
+ defp maybe_reject_anonymous(%{"actor" => actor} = object) do
+ with true <- Config.get([:mrf_remote_report, :reject_anonymous]),
+ %URI{path: "/actor"} <- URI.parse(actor) do
+ {:reject, "[RemoteReportPolicy] Anonymous: #{actor}"}
+ else
+ _ -> {:ok, object}
+ end
+ end
+
+ defp maybe_reject_third_party(%{"object" => objects} = object) do
+ {_, to} =
+ case objects do
+ [head | tail] when is_binary(head) -> {tail, head}
+ s when is_binary(s) -> {[], s}
+ _ -> {[], ""}
+ end
+
+ with true <- Config.get([:mrf_remote_report, :reject_third_party]),
+ false <- String.starts_with?(to, Pleroma.Web.Endpoint.url()) do
+ {:reject, "[RemoteReportPolicy] Third-party: #{to}"}
+ else
+ _ -> {:ok, object}
+ end
+ end
+
+ defp maybe_reject_empty_message(%{"content" => content} = object)
+ when is_binary(content) and content != "" do
+ {:ok, object}
+ end
+
+ defp maybe_reject_empty_message(object) do
+ if Config.get([:mrf_remote_report, :reject_empty_message]) do
+ {:reject, ["RemoteReportPolicy] No content"]}
+ else
+ {:ok, object}
+ end
+ end
+
+ defp local?(%{"actor" => actor}) do
+ String.starts_with?(actor, Pleroma.Web.Endpoint.url())
+ end
+
+ @impl true
+ def describe do
+ mrf_remote_report =
+ Config.get(:mrf_remote_report)
+ |> Enum.into(%{})
+
+ {:ok, %{mrf_remote_report: mrf_remote_report}}
+ end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_remote_report,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy",
+ label: "MRF Remote Report",
+ description: "Drop remote reports if they don't contain enough information.",
+ children: [
+ %{
+ key: :reject_all,
+ type: :boolean,
+ description: "Reject all remote reports? (this option takes precedence)",
+ suggestions: [false]
+ },
+ %{
+ key: :reject_anonymous,
+ type: :boolean,
+ description: "Reject anonymous remote reports?",
+ suggestions: [true]
+ },
+ %{
+ key: :reject_third_party,
+ type: :boolean,
+ description: "Reject reports on users from third-party instances?",
+ suggestions: [true]
+ },
+ %{
+ key: :reject_empty_message,
+ type: :boolean,
+ description: "Reject remote reports with no message?",
+ suggestions: [true]
+ }
+ ]
+ }
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index ae7f18bfe..a97e8db7b 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -191,6 +191,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|> MRF.instance_list_from_tuples()
end
+ @impl true
+ def id_filter(id) do
+ host_info = URI.parse(id)
+
+ with {:ok, _} <- check_accept(host_info, %{}),
+ {:ok, _} <- check_reject(host_info, %{}) do
+ true
+ else
+ _ -> false
+ end
+ end
+
@impl true
def filter(%{"type" => "Delete", "actor" => actor} = activity) do
%{host: actor_host} = URI.parse(actor)
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
index 6edfb124e..54f0e6bc1 100644
--- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -20,6 +20,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
String.match?(shortcode, pattern)
end
+ defp reject_emoji?({shortcode, _url}, installed_emoji) do
+ valid_shortcode? = String.match?(shortcode, ~r/^[a-zA-Z0-9_-]+$/)
+
+ rejected_shortcode? =
+ [:mrf_steal_emoji, :rejected_shortcodes]
+ |> Config.get([])
+ |> Enum.any?(fn pattern -> shortcode_matches?(shortcode, pattern) end)
+
+ emoji_installed? = Enum.member?(installed_emoji, shortcode)
+
+ !valid_shortcode? or rejected_shortcode? or emoji_installed?
+ end
+
defp steal_emoji({shortcode, url}, emoji_dir_path) do
url = Pleroma.Web.MediaProxy.url(url)
@@ -74,20 +87,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
)
- File.mkdir_p(emoji_dir_path)
+ Pleroma.Backports.mkdir_p(emoji_dir_path)
new_emojis =
foreign_emojis
- |> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
- |> Enum.reject(fn {shortcode, _url} -> String.contains?(shortcode, ["/", "\\"]) end)
- |> Enum.filter(fn {shortcode, _url} ->
- reject_emoji? =
- [:mrf_steal_emoji, :rejected_shortcodes]
- |> Config.get([])
- |> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)
-
- !reject_emoji?
- end)
+ |> Enum.reject(&reject_emoji?(&1, installed_emoji))
|> Enum.map(&steal_emoji(&1, emoji_dir_path))
|> Enum.filter(& &1)
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index b3043b93a..17652a0de 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -11,6 +11,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
+ import Pleroma.Constants, only: [activity_types: 0, object_types: 0]
+
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
@@ -24,6 +26,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
@@ -38,6 +41,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@impl true
def validate(object, meta)
+ # This overload works together with the InboxGuardPlug
+ # and ensures that we are not accepting any activity type
+ # that cannot pass InboxGuardPlug.
+ # If we want to support any more activity types, make sure to
+ # add it in Pleroma.Constants's activity_types or object_types,
+ # and, if applicable, allowed_activity_types_from_strangers.
+ def validate(%{"type" => type}, _meta)
+ when type not in activity_types() and type not in object_types(),
+ do: {:error, :not_allowed_object_type}
+
def validate(%{"type" => "Block"} = block_activity, meta) do
with {:ok, block_activity} <-
block_activity
@@ -103,7 +116,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
meta
)
when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do
- with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object),
+ with {:ok, object_data} <-
+ object
+ |> CommonFixes.maybe_add_language_from_activity(create_activity)
+ |> cast_and_apply_and_stringify_with_history(),
meta = Keyword.put(meta, :object_data, object_data),
{:ok, create_activity} <-
create_activity
@@ -153,11 +169,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
)
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
with {_, false} <- {:local, Access.get(meta, :local, false)},
- {_, {:ok, object_data, _}} <- {:object_validation, validate(object, meta)},
+ {_, {:ok, object_data, _}} <-
+ {:object_validation,
+ object
+ |> CommonFixes.maybe_add_language_from_activity(update_activity)
+ |> validate(meta)},
meta = Keyword.put(meta, :object_data, object_data),
{:ok, update_activity} <-
update_activity
- |> UpdateValidator.cast_and_validate()
+ |> UpdateValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do
update_activity = stringify_keys(update_activity)
{:ok, update_activity, meta}
@@ -165,7 +185,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
{:local, _} ->
with {:ok, object} <-
update_activity
- |> UpdateValidator.cast_and_validate()
+ |> UpdateValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
@@ -180,14 +200,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
def validate(%{"type" => type} = object, meta)
- when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
+ when type in ~w[Accept Reject Follow Like EmojiReact Announce
ChatMessage Answer] do
validator =
case type do
"Accept" -> AcceptRejectValidator
"Reject" -> AcceptRejectValidator
"Follow" -> FollowValidator
- "Update" -> UpdateValidator
"Like" -> LikeValidator
"EmojiReact" -> EmojiReactValidator
"Announce" -> AnnounceValidator
@@ -204,6 +223,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
+ def validate(%{"type" => type} = object, meta) when type == "Update" do
+ with {:ok, object} <-
+ object
+ |> UpdateValidator.cast_and_validate(meta)
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do
with {:ok, object} <-
object
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index 1b5b2e8fb..81ab354fe 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
def cast_and_apply(data) do
data
- |> cast_data
+ |> cast_data()
|> apply_action(:insert)
end
@@ -85,8 +85,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|> fix_replies()
|> fix_attachments()
|> CommonFixes.fix_quote_url()
+ |> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
+ |> CommonFixes.maybe_add_language()
+ |> CommonFixes.maybe_add_content_map()
end
def changeset(struct, data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex
index 65ac6bb93..034c6f33f 100644
--- a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex
@@ -100,6 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do
|> CommonFixes.fix_actor()
|> CommonFixes.fix_object_defaults()
|> CommonFixes.fix_quote_url()
+ |> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji()
|> fix_url()
|> fix_content()
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
index 1a5d02601..22cf0cc05 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
@@ -31,6 +31,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
defmacro object_fields do
quote bind_quoted: binding() do
field(:content, :string)
+ field(:contentMap, ObjectValidators.ContentLanguageMap)
field(:published, ObjectValidators.DateTime)
field(:updated, ObjectValidators.DateTime)
@@ -58,6 +59,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:quotes_count, :integer, default: 0)
+ field(:language, ObjectValidators.LanguageCode)
field(:inReplyTo, ObjectValidators.ObjectID)
field(:quoteUrl, ObjectValidators.ObjectID)
field(:url, ObjectValidators.BareUri)
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
index 4699029d4..f0f3fef90 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Language.LanguageDetector
alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Object.Containment
@@ -11,6 +12,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
+ import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode,
+ only: [good_locale_code?: 1]
+
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
+
require Pleroma.Constants
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
@@ -114,6 +120,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
def fix_quote_url(data), do: data
+ # On Mastodon, `"likes"` attribute includes an inlined `Collection` with `totalItems`,
+ # not a list of users.
+ # https://github.com/mastodon/mastodon/pull/32007
+ def fix_likes(%{"likes" => %{}} = data), do: Map.drop(data, ["likes"])
+
+ def fix_likes(data), do: data
+
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
def object_link_tag?(%{
"type" => "Link",
@@ -125,4 +138,75 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
end
def object_link_tag?(_), do: false
+
+ def maybe_add_language_from_activity(object, activity) do
+ language = get_language_from_context(activity)
+
+ if language do
+ Map.put(object, "language", language)
+ else
+ object
+ end
+ end
+
+ def maybe_add_language(object) do
+ language =
+ [
+ &get_language_from_context/1,
+ &get_language_from_content_map/1,
+ &get_language_from_content/1
+ ]
+ |> Enum.find_value(fn get_language ->
+ language = get_language.(object)
+
+ if good_locale_code?(language) do
+ language
+ else
+ nil
+ end
+ end)
+
+ if language do
+ Map.put(object, "language", language)
+ else
+ object
+ end
+ end
+
+ defp get_language_from_context(%{"@context" => context}) when is_list(context) do
+ case context
+ |> Enum.find(fn
+ %{"@language" => language} -> language != "und"
+ _ -> nil
+ end) do
+ %{"@language" => language} -> language
+ _ -> nil
+ end
+ end
+
+ defp get_language_from_context(_), do: nil
+
+ defp get_language_from_content_map(%{"contentMap" => content_map, "content" => source_content}) do
+ content_groups = Map.to_list(content_map)
+
+ case Enum.find(content_groups, fn {_, content} -> content == source_content end) do
+ {language, _} -> language
+ _ -> nil
+ end
+ end
+
+ defp get_language_from_content_map(_), do: nil
+
+ defp get_language_from_content(%{"content" => content} = object) do
+ LanguageDetector.detect("#{object["summary"] || ""} #{content}")
+ end
+
+ defp get_language_from_content(_), do: nil
+
+ def maybe_add_content_map(%{"language" => language, "content" => content} = object)
+ when not_empty_string(language) do
+ Map.put(object, "contentMap", Map.put(%{}, language, content))
+ end
+
+ def maybe_add_content_map(object), do: object
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
index ab204f69a..ea14d6aca 100644
--- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
def cast_and_apply(data) do
data
- |> cast_data
+ |> cast_data()
|> apply_action(:insert)
end
@@ -38,6 +38,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|> validate_data()
end
+ @spec cast_data(map()) :: map()
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
@@ -47,7 +48,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
data
|> CommonFixes.fix_actor()
|> CommonFixes.fix_object_defaults()
+ |> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji()
+ |> CommonFixes.maybe_add_language()
+ |> CommonFixes.maybe_add_content_map()
end
def changeset(struct, data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
index 7f9d4d648..21940f4f1 100644
--- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -64,6 +64,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|> CommonFixes.fix_actor()
|> CommonFixes.fix_object_defaults()
|> CommonFixes.fix_quote_url()
+ |> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji()
|> fix_closed()
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex
index 47cf7b415..dc2770189 100644
--- a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex
@@ -50,13 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
end
def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) do
- name =
- cond do
- "#" <> name -> name
- name -> name
- end
- |> String.downcase()
-
+ name = String.downcase(name)
data = Map.put(data, "name", name)
struct
diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
index 1e940a400..aab90235f 100644
--- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Object
+ alias Pleroma.User
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@@ -31,23 +33,50 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
|> cast(data, __schema__(:fields))
end
- defp validate_data(cng) do
+ defp validate_data(cng, meta) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Update"])
|> validate_actor_presence()
- |> validate_updating_rights()
+ |> validate_updating_rights(meta)
end
- def cast_and_validate(data) do
+ def cast_and_validate(data, meta \\ []) do
data
|> cast_data
- |> validate_data
+ |> validate_data(meta)
end
- # For now we only support updating users, and here the rule is easy:
- # object id == actor id
- def validate_updating_rights(cng) do
+ def validate_updating_rights(cng, meta) do
+ if meta[:local] do
+ validate_updating_rights_local(cng)
+ else
+ validate_updating_rights_remote(cng)
+ end
+ end
+
+ # For local Updates, verify the actor can edit the object
+ def validate_updating_rights_local(cng) do
+ actor = get_field(cng, :actor)
+ updated_object = get_field(cng, :object)
+
+ if {:ok, actor} == ObjectValidators.ObjectID.cast(updated_object) do
+ cng
+ else
+ with %User{} = user <- User.get_cached_by_ap_id(actor),
+ {_, %Object{} = orig_object} <- {:object, Object.normalize(updated_object)},
+ :ok <- Object.authorize_access(orig_object, user) do
+ cng
+ else
+ _e ->
+ cng
+ |> add_error(:object, "Can't be updated by this actor")
+ end
+ end
+ end
+
+ # For remote Updates, verify the host is the same.
+ def validate_updating_rights_remote(cng) do
with actor = get_field(cng, :actor),
object = get_field(cng, :object),
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index 0de3a0d43..cf22c4899 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -93,7 +93,20 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
- cc = Map.get(params, :cc, [])
+ param_cc = Map.get(params, :cc, [])
+
+ original_cc = Map.get(data, "cc", [])
+
+ public_address = Pleroma.Constants.as_public()
+
+ # Ensure unlisted posts don't lose the public address in the cc
+ # if the param_cc was set
+ cc =
+ if public_address in original_cc and public_address not in param_cc do
+ [public_address | param_cc]
+ else
+ param_cc
+ end
json =
data
@@ -148,17 +161,9 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
{"digest", p.digest}
]
) do
- if not is_nil(p.unreachable_since) do
- Instances.set_reachable(p.inbox)
- end
-
result
else
{_post_result, %{status: code} = response} = e ->
- if is_nil(p.unreachable_since) do
- Instances.set_unreachable(p.inbox)
- end
-
Logger.metadata(activity: p.activity_id, inbox: p.inbox, status: code)
Logger.error("Publisher failed to inbox #{p.inbox} with status #{code}")
@@ -179,10 +184,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
connection_pool_snooze()
e ->
- if is_nil(p.unreachable_since) do
- Instances.set_unreachable(p.inbox)
- end
-
Logger.metadata(activity: p.activity_id, inbox: p.inbox)
Logger.error("Publisher failed to inbox #{p.inbox} #{inspect(e)}")
{:error, e}
@@ -294,7 +295,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
[priority_recipients, recipients] = recipients(actor, activity)
- inboxes =
+ [priority_inboxes, other_inboxes] =
[priority_recipients, recipients]
|> Enum.map(fn recipients ->
recipients
@@ -307,8 +308,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
end)
Repo.checkout(fn ->
- Enum.each(inboxes, fn inboxes ->
- Enum.each(inboxes, fn {inbox, unreachable_since} ->
+ Enum.each([priority_inboxes, other_inboxes], fn inboxes ->
+ Enum.each(inboxes, fn inbox ->
%User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
# Get all the recipients on the same host and add them to cc. Otherwise, a remote
@@ -318,8 +319,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
__MODULE__.enqueue_one(%{
inbox: inbox,
cc: cc,
- activity_id: activity.id,
- unreachable_since: unreachable_since
+ activity_id: activity.id
})
end)
end)
@@ -352,12 +352,11 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|> Enum.each(fn {inboxes, priority} ->
inboxes
|> Instances.filter_reachable()
- |> Enum.each(fn {inbox, unreachable_since} ->
+ |> Enum.each(fn inbox ->
__MODULE__.enqueue_one(
%{
inbox: inbox,
- activity_id: activity.id,
- unreachable_since: unreachable_since
+ activity_id: activity.id
},
priority: priority
)
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index d6d403671..52cdc3c3f 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -145,7 +145,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user),
%User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do
- User.block(blocker, blocked)
+ User.block(blocker, blocked, Enum.into(meta, %{}))
end
{:ok, object, meta}
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 2f8a7f8f2..8819e1596 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -16,12 +16,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
import Ecto.Query
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
require Pleroma.Constants
@@ -41,6 +43,38 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_content_map()
|> fix_addressing()
|> fix_summary()
+ |> fix_history(&fix_object/1)
+ end
+
+ defp maybe_fix_object(%{"attributedTo" => _} = object), do: fix_object(object)
+ defp maybe_fix_object(object), do: object
+
+ defp fix_history(%{"formerRepresentations" => %{"orderedItems" => list}} = obj, fix_fun)
+ when is_list(list) do
+ update_in(obj["formerRepresentations"]["orderedItems"], fn h -> Enum.map(h, fix_fun) end)
+ end
+
+ defp fix_history(obj, _), do: obj
+
+ defp fix_recursive(obj, fun) do
+ # unlike Erlang, Elixir does not support recursive inline functions
+ # which would allow us to avoid reconstructing this on every recursion
+ rec_fun = fn
+ obj when is_map(obj) -> fix_recursive(obj, fun)
+ # there may be simple AP IDs in history (or object field)
+ obj -> obj
+ end
+
+ obj
+ |> fun.()
+ |> fix_history(rec_fun)
+ |> then(fn
+ %{"object" => object} = doc when is_map(object) ->
+ update_in(doc["object"], rec_fun)
+
+ apdoc ->
+ apdoc
+ end)
end
def fix_summary(%{"summary" => nil} = object) do
@@ -166,7 +200,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
quote_url =
- case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do
+ case CommonFixes.fix_quote_url(object) do
%{"quoteUrl" => quote_url} -> quote_url
_ -> nil
end
@@ -336,6 +370,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_tag(object), do: object
+ # prefer content over contentMap
+ def fix_content_map(%{"content" => content} = object) when not_empty_string(content), do: object
+
# content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
@@ -370,11 +407,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end)
end
- def handle_incoming(data, options \\ [])
+ def handle_incoming(data, options \\ []) do
+ data
+ |> fix_recursive(&strip_internal_fields/1)
+ |> handle_incoming_normalized(options)
+ end
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
# with nil ID.
- def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
+ defp handle_incoming_normalized(
+ %{"type" => "Flag", "object" => objects, "actor" => actor} = data,
+ _options
+ ) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor),
@@ -395,16 +439,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
# disallow objects with bogus IDs
- def handle_incoming(%{"id" => nil}, _options), do: :error
- def handle_incoming(%{"id" => ""}, _options), do: :error
+ defp handle_incoming_normalized(%{"id" => nil}, _options), do: :error
+ defp handle_incoming_normalized(%{"id" => ""}, _options), do: :error
# length of https:// = 8, should validate better, but good enough for now.
- def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
- do: :error
+ defp handle_incoming_normalized(%{"id" => id}, _options)
+ when is_binary(id) and byte_size(id) < 8,
+ do: :error
- def handle_incoming(
- %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
- options
- ) do
+ defp handle_incoming_normalized(
+ %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
+ options
+ ) do
actor = Containment.get_actor(data)
data =
@@ -446,25 +491,37 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"star" => "⭐"
}
- @doc "Rewrite misskey likes into EmojiReacts"
- def handle_incoming(
- %{
- "type" => "Like",
- "_misskey_reaction" => reaction
- } = data,
- options
- ) do
+ # Rewrite misskey likes into EmojiReacts
+ defp handle_incoming_normalized(
+ %{
+ "type" => "Like",
+ "content" => content
+ } = data,
+ options
+ )
+ when is_binary(content) do
data
|> Map.put("type", "EmojiReact")
- |> Map.put("content", @misskey_reactions[reaction] || reaction)
- |> handle_incoming(options)
+ |> handle_incoming_normalized(options)
end
- def handle_incoming(
- %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
- options
- )
- when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do
+ defp handle_incoming_normalized(
+ %{
+ "type" => "Like",
+ "_misskey_reaction" => reaction
+ } = data,
+ options
+ ) do
+ data
+ |> Map.put("content", @misskey_reactions[reaction] || reaction)
+ |> handle_incoming_normalized(options)
+ end
+
+ defp handle_incoming_normalized(
+ %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
+ options
+ )
+ when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object =
@@ -487,8 +544,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- def handle_incoming(%{"type" => type} = data, _options)
- when type in ~w{Like EmojiReact Announce Add Remove} do
+ defp handle_incoming_normalized(%{"type" => type} = data, _options)
+ when type in ~w{Like EmojiReact Announce Add Remove} do
with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <-
Pipeline.common_pipeline(data, local: false) do
@@ -498,11 +555,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- def handle_incoming(
- %{"type" => type} = data,
- _options
- )
- when type in ~w{Update Block Follow Accept Reject} do
+ defp handle_incoming_normalized(
+ %{"type" => type} = data,
+ _options
+ )
+ when type in ~w{Update Block Follow Accept Reject} do
+ fixed_obj = maybe_fix_object(data["object"])
+ data = if fixed_obj != nil, do: %{data | "object" => fixed_obj}, else: data
+
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{:ok, activity, _} <-
Pipeline.common_pipeline(data, local: false) do
@@ -510,10 +570,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- def handle_incoming(
- %{"type" => "Delete"} = data,
- _options
- ) do
+ defp handle_incoming_normalized(
+ %{"type" => "Delete"} = data,
+ _options
+ ) do
with {:ok, activity, _} <-
Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
@@ -536,15 +596,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- def handle_incoming(
- %{
- "type" => "Undo",
- "object" => %{"type" => "Follow", "object" => followed},
- "actor" => follower,
- "id" => id
- } = _data,
- _options
- ) do
+ defp handle_incoming_normalized(
+ %{
+ "type" => "Undo",
+ "object" => %{"type" => "Follow", "object" => followed},
+ "actor" => follower,
+ "id" => id
+ } = _data,
+ _options
+ ) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
@@ -555,46 +615,46 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- def handle_incoming(
- %{
- "type" => "Undo",
- "object" => %{"type" => type}
- } = data,
- _options
- )
- when type in ["Like", "EmojiReact", "Announce", "Block"] do
+ defp handle_incoming_normalized(
+ %{
+ "type" => "Undo",
+ "object" => %{"type" => type}
+ } = data,
+ _options
+ )
+ when type in ["Like", "EmojiReact", "Announce", "Block"] do
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
end
end
# For Undos that don't have the complete object attached, try to find it in our database.
- def handle_incoming(
- %{
- "type" => "Undo",
- "object" => object
- } = activity,
- options
- )
- when is_binary(object) do
+ defp handle_incoming_normalized(
+ %{
+ "type" => "Undo",
+ "object" => object
+ } = activity,
+ options
+ )
+ when is_binary(object) do
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
activity
|> Map.put("object", data)
- |> handle_incoming(options)
+ |> handle_incoming_normalized(options)
else
_e -> :error
end
end
- def handle_incoming(
- %{
- "type" => "Move",
- "actor" => origin_actor,
- "object" => origin_actor,
- "target" => target_actor
- },
- _options
- ) do
+ defp handle_incoming_normalized(
+ %{
+ "type" => "Move",
+ "actor" => origin_actor,
+ "object" => origin_actor,
+ "target" => target_actor
+ },
+ _options
+ ) do
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
true <- origin_actor in target_user.also_known_as do
@@ -604,7 +664,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- def handle_incoming(_, _), do: :error
+ # Rewrite dislikes into the thumbs down emoji
+ defp handle_incoming_normalized(%{"type" => "Dislike"} = data, options) do
+ data
+ |> Map.put("type", "EmojiReact")
+ |> Map.put("content", "👎")
+ |> handle_incoming_normalized(options)
+ end
+
+ defp handle_incoming_normalized(
+ %{"type" => "Undo", "object" => %{"type" => "Dislike"}} = data,
+ options
+ ) do
+ data
+ |> put_in(["object", "type"], "EmojiReact")
+ |> put_in(["object", "content"], "👎")
+ |> handle_incoming_normalized(options)
+ end
+
+ defp handle_incoming_normalized(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
@@ -716,6 +794,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> set_reply_to_uri
|> set_quote_url
|> set_replies
+ |> CommonFixes.maybe_add_content_map()
|> strip_internal_fields
|> strip_internal_tags
|> set_type
@@ -750,12 +829,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object_id
|> Object.normalize(fetch: false)
|> Map.get(:data)
- |> prepare_object
data =
data
- |> Map.put("object", object)
- |> Map.merge(Utils.make_json_ld_header())
+ |> Map.put("object", prepare_object(object))
+ |> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
@@ -763,14 +841,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.updatable_object_types() do
- object =
- object
- |> prepare_object
-
data =
data
- |> Map.put("object", object)
- |> Map.merge(Utils.make_json_ld_header())
+ |> Map.put("object", prepare_object(object))
+ |> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
@@ -840,7 +914,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data
|> strip_internal_fields
|> maybe_fix_object_url
- |> Map.merge(Utils.make_json_ld_header())
+ |> Map.merge(Utils.make_json_ld_header(data))
{:ok, data}
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 6c792804d..f30c92abf 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -20,6 +20,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.Web.Router.Helpers
import Ecto.Query
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
require Logger
require Pleroma.Constants
@@ -109,18 +110,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do
end
end
- def make_json_ld_header do
+ def make_json_ld_header(data \\ %{}) do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
%{
- "@language" => "und"
+ "@language" => get_language(data)
}
]
}
end
+ defp get_language(%{"language" => language}) when not_empty_string(language) do
+ language
+ end
+
+ defp get_language(_), do: "und"
+
def make_date do
DateTime.utc_now() |> DateTime.to_iso8601()
end
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index 63caa915c..13b5b2542 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
alias Pleroma.Web.ActivityPub.Transmogrifier
def render("object.json", %{object: %Object{} = object}) do
- base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
+ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(object.data)
additional = Transmogrifier.prepare_object(object.data)
Map.merge(base, additional)
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
when activity_type in ["Create", "Listen"] do
- base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
+ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
object = Object.normalize(activity, fetch: false)
additional =
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
end
def render("object.json", %{object: %Activity{} = activity}) do
- base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
+ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
object_id = Object.normalize(activity, id_only: true)
additional =
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 937e4fd67..61975387b 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -127,10 +127,25 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"capabilities" => capabilities,
"alsoKnownAs" => user.also_known_as,
"vcard:bday" => birthday,
- "webfinger" => "acct:#{User.full_nickname(user)}"
+ "webfinger" => "acct:#{User.full_nickname(user)}",
+ "published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at)
}
- |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
- |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
+ |> Map.merge(
+ maybe_make_image(
+ &User.avatar_url/2,
+ User.image_description(user.avatar, nil),
+ "icon",
+ user
+ )
+ )
+ |> Map.merge(
+ maybe_make_image(
+ &User.banner_url/2,
+ User.image_description(user.banner, nil),
+ "image",
+ user
+ )
+ )
|> Map.merge(Utils.make_json_ld_header())
end
@@ -305,16 +320,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
end
end
- defp maybe_make_image(func, key, user) do
+ defp maybe_make_image(func, description, key, user) do
if image = func.(user, no_default: true) do
%{
- key => %{
- "type" => "Image",
- "url" => image
- }
+ key =>
+ %{
+ "type" => "Image",
+ "url" => image
+ }
+ |> maybe_put_description(description)
}
else
%{}
end
end
+
+ defp maybe_put_description(map, description) when is_binary(description) do
+ Map.put(map, "name", description)
+ end
+
+ defp maybe_put_description(map, _description), do: map
end
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index 0f22dd538..b35f5cdcd 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -335,13 +335,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
if params["password"] do
User.force_password_reset_async(user)
- end
- ModerationLog.insert_log(%{
- actor: admin,
- subject: [user],
- action: "force_password_reset"
- })
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: [user],
+ action: "force_password_reset"
+ })
+ end
json(conn, %{status: "success"})
else
diff --git a/lib/pleroma/web/admin_api/controllers/instance_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_controller.ex
index 117a72280..40d4d812e 100644
--- a/lib/pleroma/web/admin_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/instance_controller.ex
@@ -49,7 +49,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceController do
end
def delete(conn, %{"instance" => instance}) do
- with {:ok, _job} <- Instance.delete_users_and_activities(instance) do
+ with {:ok, _job} <- Instance.delete(instance) do
json(conn, instance)
end
end
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index 314782818..3e0ac3704 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -97,7 +97,7 @@ defmodule Pleroma.Web.ApiSpec do
"Frontend management",
"Instance configuration",
"Instance documents",
- "Instance rule managment",
+ "Instance rule management",
"Invites",
"MediaProxy cache",
"OAuth application management",
@@ -139,7 +139,8 @@ defmodule Pleroma.Web.ApiSpec do
"Search",
"Status actions",
"Media attachments",
- "Bookmark folders"
+ "Bookmark folders",
+ "Tags"
]
},
%{
@@ -157,6 +158,6 @@ defmodule Pleroma.Web.ApiSpec do
}
}
# discover request/response schemas from path specs
- |> OpenApiSpex.resolve_schema_modules()
+ |> then(&OpenApiSpex.resolve_schema_modules/1)
end
end
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 21a779dcb..d63e92d16 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -284,18 +284,6 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
:query,
%Schema{allOf: [BooleanLike], default: true},
"Mute notifications in addition to statuses? Defaults to `true`."
- ),
- Operation.parameter(
- :duration,
- :query,
- %Schema{type: :integer},
- "Expire the mute in `duration` seconds. Default 0 for infinity"
- ),
- Operation.parameter(
- :expires_in,
- :query,
- %Schema{type: :integer, default: 0},
- "Deprecated, use `duration` instead"
)
],
responses: %{
@@ -323,16 +311,37 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
tags: ["Account actions"],
summary: "Block",
operationId: "AccountController.block",
+ requestBody: request_body("Parameters", block_request()),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
description:
"Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
- parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
+ ],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end
+ defp block_request do
+ %Schema{
+ title: "AccountBlockRequest",
+ description: "POST body for blocking an account",
+ type: :object,
+ properties: %{
+ duration: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "Expire the mute in `duration` seconds. Default 0 for infinity"
+ }
+ },
+ example: %{
+ "duration" => 86_400
+ }
+ }
+ end
+
def unblock_operation do
%Operation{
tags: ["Account actions"],
diff --git a/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
index c3a3ecc7c..6d06728f4 100644
--- a/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
def index_operation do
%Operation{
- tags: ["Instance rule managment"],
+ tags: ["Instance rule management"],
summary: "Retrieve list of instance rules",
operationId: "AdminAPI.RuleController.index",
security: [%{"oAuth" => ["admin:read"]}],
@@ -33,7 +33,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
def create_operation do
%Operation{
- tags: ["Instance rule managment"],
+ tags: ["Instance rule management"],
summary: "Create new rule",
operationId: "AdminAPI.RuleController.create",
security: [%{"oAuth" => ["admin:write"]}],
@@ -49,7 +49,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
def update_operation do
%Operation{
- tags: ["Instance rule managment"],
+ tags: ["Instance rule management"],
summary: "Modify existing rule",
operationId: "AdminAPI.RuleController.update",
security: [%{"oAuth" => ["admin:write"]}],
@@ -65,7 +65,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
def delete_operation do
%Operation{
- tags: ["Instance rule managment"],
+ tags: ["Instance rule management"],
summary: "Delete rule",
operationId: "AdminAPI.RuleController.delete",
parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex
index 7d7a5ecc1..911ffb994 100644
--- a/lib/pleroma/web/api_spec/operations/instance_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex
@@ -52,7 +52,30 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
summary: "Retrieve list of instance rules",
operationId: "InstanceController.rules",
responses: %{
- 200 => Operation.response("Array of domains", "application/json", array_of_rules())
+ 200 => Operation.response("Array of rules", "application/json", array_of_rules())
+ }
+ }
+ end
+
+ def translation_languages_operation do
+ %Operation{
+ tags: ["Instance misc"],
+ summary: "Retrieve supported languages matrix",
+ operationId: "InstanceController.translation_languages",
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Translation languages matrix",
+ "application/json",
+ %Schema{
+ type: :object,
+ additionalProperties: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Supported target languages for a source language"
+ }
+ }
+ )
}
}
end
diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex
index e6df21246..588b42e06 100644
--- a/lib/pleroma/web/api_spec/operations/media_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/media_operation.ex
@@ -121,7 +121,7 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do
security: [%{"oAuth" => ["write:media"]}],
requestBody: Helpers.request_body("Parameters", create_request()),
responses: %{
- 202 => Operation.response("Media", "application/json", Attachment),
+ 200 => Operation.response("Media", "application/json", Attachment),
400 => Operation.response("Media", "application/json", ApiError),
422 => Operation.response("Media", "application/json", ApiError),
500 => Operation.response("Media", "application/json", ApiError)
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
index efa36ffdc..dd503997a 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
@@ -127,6 +127,20 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
}
end
+ def download_zip_operation do
+ %Operation{
+ tags: ["Emoji pack administration"],
+ summary: "Download a pack from a URL or an uploaded file",
+ operationId: "PleromaAPI.EmojiPackController.download_zip",
+ security: [%{"oAuth" => ["admin:write"]}],
+ requestBody: request_body("Parameters", download_zip_request(), required: true),
+ responses: %{
+ 200 => ok_response(),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
defp download_request do
%Schema{
type: :object,
@@ -143,6 +157,25 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
}
end
+ defp download_zip_request do
+ %Schema{
+ type: :object,
+ required: [:name],
+ properties: %{
+ url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "URL of the file"
+ },
+ file: %Schema{
+ description: "The uploaded ZIP file",
+ type: :object
+ },
+ name: %Schema{type: :string, format: :uri, description: "Pack Name"}
+ }
+ }
+ end
+
def create_operation do
%Operation{
tags: ["Emoji pack administration"],
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
index f595583b6..6f77584a8 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
@@ -59,11 +59,15 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
album: %Schema{type: :string, description: "The album of the media playing"},
artist: %Schema{type: :string, description: "The artist of the media playing"},
length: %Schema{type: :integer, description: "The length of the media playing"},
- externalLink: %Schema{type: :string, description: "A URL referencing the media playing"},
+ external_link: %Schema{type: :string, description: "A URL referencing the media playing"},
visibility: %Schema{
allOf: [VisibilityScope],
default: "public",
description: "Scrobble visibility"
+ },
+ externalLink: %Schema{
+ type: :string,
+ description: "Deprecated, use `external_link` instead"
}
},
example: %{
@@ -71,7 +75,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
"artist" => "Some Artist",
"album" => "Some Album",
"length" => 180_000,
- "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title"
+ "external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title"
}
}
end
@@ -85,7 +89,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
title: %Schema{type: :string, description: "The title of the media playing"},
album: %Schema{type: :string, description: "The album of the media playing"},
artist: %Schema{type: :string, description: "The artist of the media playing"},
- externalLink: %Schema{type: :string, description: "A URL referencing the media playing"},
+ external_link: %Schema{type: :string, description: "A URL referencing the media playing"},
length: %Schema{
type: :integer,
description: "The length of the media playing",
@@ -100,7 +104,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
"artist" => "Some Artist",
"album" => "Some Album",
"length" => 180_000,
- "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title",
+ "external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title",
"created_at" => "2019-09-28T12:40:45.000Z"
}
}
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index ef828feee..75ecda321 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -427,6 +427,38 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
}
end
+ def translate_operation do
+ %Operation{
+ tags: ["Retrieve status information"],
+ summary: "Translate status",
+ description: "Translate status with an external API",
+ operationId: "StatusController.translate",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param()],
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ properties: %{
+ lang: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Translation target language."
+ }
+ }
+ },
+ required: false
+ ),
+ responses: %{
+ 200 => Operation.response("Translation", "application/json", translation()),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError),
+ 503 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
def favourites_operation do
%Operation{
tags: ["Timelines"],
@@ -819,4 +851,32 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
}
}
end
+
+ defp translation do
+ %Schema{
+ title: "StatusTranslation",
+ description: "Represents status translation with related information.",
+ type: :object,
+ required: [:content, :detected_source_language, :provider],
+ properties: %{
+ content: %Schema{
+ type: :string,
+ description: "Translated status content"
+ },
+ detected_source_language: %Schema{
+ type: :string,
+ description: "Detected source language"
+ },
+ provider: %Schema{
+ type: :string,
+ description: "Translation provider service name"
+ }
+ },
+ example: %{
+ "content" => "Software für die nächste Generation der sozialen Medien.",
+ "detected_source_language" => "en",
+ "provider" => "Deepl"
+ }
+ }
+ end
end
diff --git a/lib/pleroma/web/api_spec/operations/tag_operation.ex b/lib/pleroma/web/api_spec/operations/tag_operation.ex
new file mode 100644
index 000000000..ce4f4ad5b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/tag_operation.ex
@@ -0,0 +1,103 @@
+defmodule Pleroma.Web.ApiSpec.TagOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.Tag
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Tags"],
+ summary: "Hashtag",
+ description: "View a hashtag",
+ security: [%{"oAuth" => ["read"]}],
+ parameters: [id_param()],
+ operationId: "TagController.show",
+ responses: %{
+ 200 => Operation.response("Hashtag", "application/json", Tag),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def follow_operation do
+ %Operation{
+ tags: ["Tags"],
+ summary: "Follow a hashtag",
+ description: "Follow a hashtag",
+ security: [%{"oAuth" => ["write:follows"]}],
+ parameters: [id_param()],
+ operationId: "TagController.follow",
+ responses: %{
+ 200 => Operation.response("Hashtag", "application/json", Tag),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unfollow_operation do
+ %Operation{
+ tags: ["Tags"],
+ summary: "Unfollow a hashtag",
+ description: "Unfollow a hashtag",
+ security: [%{"oAuth" => ["write:follows"]}],
+ parameters: [id_param()],
+ operationId: "TagController.unfollow",
+ responses: %{
+ 200 => Operation.response("Hashtag", "application/json", Tag),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_followed_operation do
+ %Operation{
+ tags: ["Tags"],
+ summary: "Followed hashtags",
+ description: "View a list of hashtags the currently authenticated user is following",
+ parameters: pagination_params(),
+ security: [%{"oAuth" => ["read:follows"]}],
+ operationId: "TagController.show_followed",
+ responses: %{
+ 200 =>
+ Operation.response("Hashtags", "application/json", %Schema{
+ type: :array,
+ items: Tag
+ }),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(
+ :id,
+ :path,
+ %Schema{type: :string},
+ "Name of the hashtag"
+ )
+ end
+
+ def pagination_params do
+ [
+ Operation.parameter(:max_id, :query, :integer, "Return items older than this ID"),
+ Operation.parameter(
+ :min_id,
+ :query,
+ :integer,
+ "Return the oldest items newer than this ID"
+ ),
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer, default: 20},
+ "Maximum number of items to return. Will be ignored if it's more than 40"
+ )
+ ]
+ end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index 1f73ef60c..19827e996 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -34,6 +34,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
id: FlakeID,
locked: %Schema{type: :boolean},
mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
+ block_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
note: %Schema{type: :string, format: :html},
statuses_count: %Schema{type: :integer},
url: %Schema{type: :string, format: :uri},
diff --git a/lib/pleroma/web/api_spec/schemas/tag.ex b/lib/pleroma/web/api_spec/schemas/tag.ex
index 66bf0ca71..05ff10cd3 100644
--- a/lib/pleroma/web/api_spec/schemas/tag.ex
+++ b/lib/pleroma/web/api_spec/schemas/tag.ex
@@ -17,11 +17,22 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
type: :string,
format: :uri,
description: "A link to the hashtag on the instance"
+ },
+ following: %Schema{
+ type: :boolean,
+ description: "Whether the authenticated user is following the hashtag"
+ },
+ history: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description:
+ "A list of historical uses of the hashtag (not implemented, for compatibility only)"
}
},
example: %{
name: "cofe",
- url: "https://lain.com/tag/cofe"
+ url: "https://lain.com/tag/cofe",
+ following: false
}
})
end
diff --git a/lib/pleroma/web/api_spec/scopes/compiler.ex b/lib/pleroma/web/api_spec/scopes/compiler.ex
index 162edc9a3..a92cf1199 100644
--- a/lib/pleroma/web/api_spec/scopes/compiler.ex
+++ b/lib/pleroma/web/api_spec/scopes/compiler.ex
@@ -26,7 +26,11 @@ defmodule Pleroma.Web.ApiSpec.Scopes.Compiler do
end
def extract_all_scopes do
- extract_all_scopes_from(Pleroma.Web.ApiSpec.spec())
+ try do
+ extract_all_scopes_from(Pleroma.Web.ApiSpec.spec())
+ catch
+ _, _ -> []
+ end
end
def extract_all_scopes_from(specs) do
diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex
index 01bf1575c..95be892cd 100644
--- a/lib/pleroma/web/auth/authenticator.ex
+++ b/lib/pleroma/web/auth/authenticator.ex
@@ -10,4 +10,9 @@ defmodule Pleroma.Web.Auth.Authenticator do
@callback handle_error(Plug.Conn.t(), any()) :: any()
@callback auth_template() :: String.t() | nil
@callback oauth_consumer_template() :: String.t() | nil
+
+ @callback change_password(Pleroma.User.t(), String.t(), String.t(), String.t()) ::
+ {:ok, Pleroma.User.t()} | {:error, term()}
+
+ @optional_callbacks change_password: 4
end
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index c420c8bc3..ec6601fb9 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.LDAPAuthenticator do
+ alias Pleroma.LDAP
alias Pleroma.User
import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1]
@@ -19,7 +20,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
def get_user(%Plug.Conn{} = conn) do
with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])},
{:ok, {name, password}} <- fetch_credentials(conn),
- %User{} = user <- GenServer.call(Pleroma.LDAP, {:bind_user, name, password}) do
+ %User{} = user <- LDAP.bind_user(name, password) do
{:ok, user}
else
{:ldap, _} ->
@@ -29,4 +30,13 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
error
end
end
+
+ def change_password(user, password, new_password, new_password) do
+ case LDAP.change_password(user.nickname, password, new_password) do
+ :ok -> {:ok, user}
+ e -> e
+ end
+ end
+
+ def change_password(_, _, _, _), do: {:error, :password_confirmation}
end
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index 09a58eb66..0da3f19fc 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.AuthenticationPlug
import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1]
@@ -101,4 +102,23 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
def auth_template, do: nil
def oauth_consumer_template, do: nil
+
+ @doc "Changes Pleroma.User password in the database"
+ def change_password(user, password, new_password, new_password) do
+ case CommonAPI.Utils.confirm_current_password(user, password) do
+ {:ok, user} ->
+ with {:ok, _user} <-
+ User.reset_password(user, %{
+ password: new_password,
+ password_confirmation: new_password
+ }) do
+ {:ok, user}
+ end
+
+ error ->
+ error
+ end
+ end
+
+ def change_password(_, _, _, _), do: {:error, :password_confirmation}
end
diff --git a/lib/pleroma/web/auth/wrapper_authenticator.ex b/lib/pleroma/web/auth/wrapper_authenticator.ex
index a077cfa41..97b901036 100644
--- a/lib/pleroma/web/auth/wrapper_authenticator.ex
+++ b/lib/pleroma/web/auth/wrapper_authenticator.ex
@@ -39,4 +39,8 @@ defmodule Pleroma.Web.Auth.WrapperAuthenticator do
implementation().oauth_consumer_template() ||
Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")
end
+
+ @impl true
+ def change_password(user, password, new_password, new_password_confirmation),
+ do: implementation().change_password(user, password, new_password, new_password_confirmation)
end
diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex
index 412424dae..ae554d0b9 100644
--- a/lib/pleroma/web/common_api.ex
+++ b/lib/pleroma/web/common_api.ex
@@ -27,9 +27,9 @@ defmodule Pleroma.Web.CommonAPI do
require Logger
@spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
- def block(blocked, blocker) do
- with {:ok, block_data, _} <- Builder.block(blocker, blocked),
- {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
+ def block(blocked, blocker, params \\ %{}) do
+ with {:ok, block_data, meta} <- Builder.block(blocker, blocked, params),
+ {:ok, block, _} <- Pipeline.common_pipeline(block_data, meta ++ [local: true]) do
{:ok, block}
end
end
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 8aa1e258d..f60ed8b02 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -5,12 +5,16 @@
defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
+ alias Pleroma.Language.LanguageDetector
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
+ import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode,
+ only: [good_locale_code?: 1]
+
import Pleroma.Web.Gettext
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
@@ -38,6 +42,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
cc: [],
context: nil,
sensitive: false,
+ language: nil,
object: nil,
preview?: false,
changes: %{}
@@ -64,6 +69,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> content()
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
+ |> with_valid(&language/1)
|> sensitive()
|> with_valid(&object/1)
|> preview?()
@@ -85,7 +91,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp listen_object(draft) do
object =
draft.params
- |> Map.take([:album, :artist, :title, :length, :externalLink])
+ |> Map.take([:album, :artist, :title, :length])
+ |> Map.put(:externalLink, Map.get(draft.params, :external_link))
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Audio")
|> Map.put("to", draft.to)
@@ -249,6 +256,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | sensitive: sensitive}
end
+ defp language(draft) do
+ language =
+ with language <- draft.params[:language],
+ true <- good_locale_code?(language) do
+ language
+ else
+ _ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary)
+ end
+
+ %__MODULE__{draft | language: language}
+ end
+
defp object(draft) do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
@@ -288,6 +307,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
"mediaType" => Utils.get_content_type(draft.params[:content_type])
})
|> Map.put("generator", draft.params[:generator])
+ |> Map.put("language", draft.language)
%__MODULE__{draft | object: object}
end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index fef907ace..bab3c9fd0 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.Endpoint do
websocket: [
path: "/",
compress: false,
+ connect_info: [:sec_websocket_protocol],
error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []},
fullsweep_after: 20
]
diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex
index 4a0885fab..6637848a9 100644
--- a/lib/pleroma/web/fallback/redirect_controller.ex
+++ b/lib/pleroma/web/fallback/redirect_controller.ex
@@ -46,7 +46,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
redirector_with_meta(conn, %{user: user})
else
nil ->
- redirector(conn, params)
+ redirector_with_meta(conn, Map.delete(params, "maybe_nickname_or_id"))
end
end
diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex
index 58260afa8..676fc5137 100644
--- a/lib/pleroma/web/federator.ex
+++ b/lib/pleroma/web/federator.ex
@@ -122,6 +122,10 @@ defmodule Pleroma.Web.Federator do
Logger.debug("Unhandled actor #{actor}, #{inspect(e)}")
{:error, e}
+ {:reject, reason} = e ->
+ Logger.debug("Rejected by MRF: #{inspect(reason)}")
+ {:error, e}
+
e ->
# Just drop those for now
Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)
diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex
index e60767327..02d639296 100644
--- a/lib/pleroma/web/feed/tag_controller.ex
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.Feed.TagController do
alias Pleroma.Web.Feed.FeedView
def feed(conn, params) do
- if Config.get!([:instance, :public]) do
+ if not Config.restrict_unauthenticated_access?(:timelines, :local) do
render_feed(conn, params)
else
render_error(conn, :not_found, "Not found")
@@ -18,10 +18,12 @@ defmodule Pleroma.Web.Feed.TagController do
end
defp render_feed(conn, %{"tag" => raw_tag} = params) do
+ local_only = Config.restrict_unauthenticated_access?(:timelines, :federated)
+
{format, tag} = parse_tag(raw_tag)
activities =
- %{type: ["Create"], tag: tag}
+ %{type: ["Create"], tag: tag, local_only: local_only}
|> Pleroma.Maps.put_if_present(:max_id, params["max_id"])
|> ActivityPub.fetch_public_activities()
diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index 6657c2b3e..304313068 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -15,11 +15,11 @@ defmodule Pleroma.Web.Feed.UserController do
action_fallback(:errors)
- def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
+ def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname} = params) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, %{user: user})
else
- _ -> Pleroma.Web.Fallback.RedirectController.redirector(conn, nil)
+ _ -> Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, params)
end
end
diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex
index 9da3c5008..143a0b0b8 100644
--- a/lib/pleroma/web/instance_document.ex
+++ b/lib/pleroma/web/instance_document.ex
@@ -46,7 +46,7 @@ defmodule Pleroma.Web.InstanceDocument do
defp put_file(origin_path, destination_path) do
with destination <- instance_static_dir(destination_path),
- {_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))},
+ {_, :ok} <- {:mkdir_p, Pleroma.Backports.mkdir_p(Path.dirname(destination))},
{_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do
:ok
else
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 68157b0c4..d374e8c01 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -501,8 +501,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/accounts/:id/block"
- def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
- with {:ok, _activity} <- CommonAPI.block(blocked, blocker) do
+ def block(
+ %{
+ assigns: %{user: blocker, account: blocked},
+ private: %{open_api_spex: %{body_params: params}}
+ } = conn,
+ _params
+ ) do
+ with {:ok, _activity} <- CommonAPI.block(blocked, blocker, params) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -607,7 +613,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
users: users,
for: user,
as: :user,
- embed_relationships: embed_relationships?(params)
+ embed_relationships: embed_relationships?(params),
+ blocks: true
)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
index b97b0e476..0f74c1dff 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -30,4 +30,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
def rules(conn, _params) do
render(conn, "rules.json")
end
+
+ @doc "GET /api/v1/instance/translation_languages"
+ def translation_languages(conn, _params) do
+ render(conn, "translation_languages.json")
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index 056bad844..41056d389 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -53,9 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
) do
attachment_data = Map.put(object.data, "id", object.id)
- conn
- |> put_status(202)
- |> render("attachment.json", %{attachment: attachment_data})
+ render(conn, "attachment.json", %{attachment: attachment_data})
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index a2af8148c..6526457df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Workers.PollWorker
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@@ -27,12 +28,16 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+ @poll_refresh_interval 120
@doc "GET /api/v1/polls/:id"
def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
- with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+ with %Object{} = object <- Object.get_by_id(id),
+ %Activity{} = activity <-
+ Activity.get_create_by_object_ap_id_with_object(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
+ maybe_refresh_poll(activity)
+
try_render(conn, "show.json", %{object: object, for: user})
else
error when is_nil(error) or error == false ->
@@ -70,4 +75,13 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
end
end)
end
+
+ defp maybe_refresh_poll(%Activity{object: %Object{} = object} = activity) do
+ with false <- activity.local,
+ {:ok, end_time} <- NaiveDateTime.from_iso8601(object.data["closed"]),
+ {_, :lt} <- {:closed_compare, NaiveDateTime.compare(object.updated_at, end_time)} do
+ PollWorker.new(%{"op" => "refresh", "activity_id" => activity.id})
+ |> Oban.insert(unique: [period: @poll_refresh_interval])
+ end
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index 628aa311b..53f1216fd 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller
+ alias Pleroma.Hashtag
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ControllerHelper
@@ -120,69 +121,14 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp resource_search(:v2, "hashtags", query, options) do
tags_path = Endpoint.url() <> "/tag/"
- query
- |> prepare_tags(options)
+ Hashtag.search(query, options)
|> Enum.map(fn tag ->
%{name: tag, url: tags_path <> tag}
end)
end
defp resource_search(:v1, "hashtags", query, options) do
- prepare_tags(query, options)
- end
-
- defp prepare_tags(query, options) do
- tags =
- query
- |> preprocess_uri_query()
- |> String.split(~r/[^#\w]+/u, trim: true)
- |> Enum.uniq_by(&String.downcase/1)
-
- explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
-
- tags =
- if Enum.any?(explicit_tags) do
- explicit_tags
- else
- tags
- end
-
- tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
-
- tags =
- if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
- add_joined_tag(tags)
- else
- tags
- end
-
- Pleroma.Pagination.paginate_list(tags, options)
- end
-
- defp add_joined_tag(tags) do
- tags
- |> Kernel.++([joined_tag(tags)])
- |> Enum.uniq_by(&String.downcase/1)
- end
-
- # If `query` is a URI, returns last component of its path, otherwise returns `query`
- defp preprocess_uri_query(query) do
- if query =~ ~r/https?:\/\// do
- query
- |> String.trim_trailing("/")
- |> URI.parse()
- |> Map.get(:path)
- |> String.split("/")
- |> Enum.at(-1)
- else
- query
- end
- end
-
- defp joined_tag(tags) do
- tags
- |> Enum.map(fn tag -> String.capitalize(tag) end)
- |> Enum.join()
+ Hashtag.search(query, options)
end
defp with_fallback(f, fallback \\ []) do
@@ -190,7 +136,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
f.()
rescue
error ->
- Logger.error("#{__MODULE__} search error: #{inspect(error)}")
+ Logger.error(Exception.format(:error, error, __STACKTRACE__))
fallback
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index d5aef5ad2..32874d464 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.BookmarkFolder
+ alias Pleroma.Language.Translation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
@@ -44,6 +45,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
]
)
+ plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :translate)
+
plug(
OAuthScopesPlug,
%{scopes: ["write:statuses"]}
@@ -85,7 +88,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
)
- @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
+ @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete translate)a
plug(
RateLimiter,
@@ -549,6 +552,44 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
end
+ @doc "POST /api/v1/statuses/:id/translate"
+ def translate(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: params, params: %{id: status_id}}}
+ } = conn,
+ _
+ ) do
+ with %Activity{object: object} <- Activity.get_by_id_with_object(status_id),
+ {:visibility, visibility} when visibility in ["public", "unlisted"] <-
+ {:visibility, Visibility.get_visibility(object)},
+ {:language, language} when is_binary(language) <-
+ {:language, Map.get(params, :lang) || user.language},
+ {:ok, result} <-
+ Translation.translate(
+ object.data["content"],
+ object.data["language"],
+ language
+ ) do
+ render(conn, "translation.json", result)
+ else
+ {:language, nil} ->
+ render_error(conn, :bad_request, "Language not specified")
+
+ {:visibility, _} ->
+ render_error(conn, :not_found, "Record not found")
+
+ {:error, :not_found} ->
+ render_error(conn, :not_found, "Translation service not configured")
+
+ {:error, error} when error in [:unexpected_response, :quota_exceeded, :too_many_requests] ->
+ render_error(conn, :service_unavailable, "Translation service not available")
+
+ _ ->
+ render_error(conn, :internal_server_error, "Translation failed")
+ end
+ end
+
@doc "GET /api/v1/favourites"
def favourites(
%{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,
diff --git a/lib/pleroma/web/mastodon_api/controllers/tag_controller.ex b/lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
new file mode 100644
index 000000000..21c21e984
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
@@ -0,0 +1,77 @@
+defmodule Pleroma.Web.MastodonAPI.TagController do
+ @moduledoc "Hashtag routes for mastodon API"
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Hashtag
+ alias Pleroma.Pagination
+ alias Pleroma.User
+
+ import Pleroma.Web.ControllerHelper,
+ only: [
+ add_link_headers: 2
+ ]
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["read"]} when action in [:show]
+ )
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["read:follows"]} when action in [:show_followed]
+ )
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["write:follows"]} when action in [:follow, :unfollow]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TagOperation
+
+ def show(conn, %{id: id}) do
+ with %Hashtag{} = hashtag <- Hashtag.get_by_name(id) do
+ render(conn, "show.json", tag: hashtag, for_user: conn.assigns.user)
+ else
+ _ -> conn |> render_error(:not_found, "Hashtag not found")
+ end
+ end
+
+ def follow(conn, %{id: id}) do
+ with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
+ %User{} = user <- conn.assigns.user,
+ {:ok, _} <-
+ User.follow_hashtag(user, hashtag) do
+ render(conn, "show.json", tag: hashtag, for_user: user)
+ else
+ _ -> render_error(conn, :not_found, "Hashtag not found")
+ end
+ end
+
+ def unfollow(conn, %{id: id}) do
+ with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
+ %User{} = user <- conn.assigns.user,
+ {:ok, _} <-
+ User.unfollow_hashtag(user, hashtag) do
+ render(conn, "show.json", tag: hashtag, for_user: user)
+ else
+ _ -> render_error(conn, :not_found, "Hashtag not found")
+ end
+ end
+
+ def show_followed(conn, params) do
+ with %{assigns: %{user: %User{} = user}} <- conn do
+ params = Map.put(params, :id_type, :integer)
+
+ hashtags =
+ user
+ |> User.HashtagFollow.followed_hashtags_query()
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> add_link_headers(hashtags)
+ |> render("index.json", tags: hashtags, for_user: user)
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 293c61b41..5ee74a80e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -40,6 +40,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
# GET /api/v1/timelines/home
def home(%{assigns: %{user: user}} = conn, params) do
+ followed_hashtags =
+ user
+ |> User.followed_hashtags()
+ |> Enum.map(& &1.id)
+
params =
params
|> Map.put(:type, ["Create", "Announce"])
@@ -49,6 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:announce_filtering_user, user)
|> Map.put(:user, user)
|> Map.put(:local_only, params[:local])
+ |> Map.put(:followed_hashtags, followed_hashtags)
|> Map.delete(:local)
activities =
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 7de6745d4..03a2fc55a 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -168,9 +168,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
UserRelationship.exists?(
user_relationships,
:endorsement,
- target,
reading_user,
- &User.endorses?(&2, &1)
+ target,
+ &User.endorses?(&1, &2)
)
}
end
@@ -219,10 +219,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
avatar = User.avatar_url(user) |> MediaProxy.url()
avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true)
- avatar_description = image_description(user.avatar)
+ avatar_description = User.image_description(user.avatar)
header = User.banner_url(user) |> MediaProxy.url()
header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
- header_description = image_description(user.banner)
+ header_description = User.image_description(user.banner)
following_count =
if !user.hide_follows_count or !user.hide_follows or self,
@@ -340,6 +340,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_unread_notification_count(user, opts[:for])
|> maybe_put_email_address(user, opts[:for])
|> maybe_put_mute_expires_at(user, opts[:for], opts)
+ |> maybe_put_block_expires_at(user, opts[:for], opts)
|> maybe_show_birthday(user, opts[:for])
end
@@ -349,10 +350,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp username_from_nickname(_), do: nil
- defp image_description(%{"name" => name}), do: name
-
- defp image_description(_), do: ""
-
defp maybe_put_follow_requests_count(
data,
%User{id: user_id} = user,
@@ -480,6 +477,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_mute_expires_at(data, _, _, _), do: data
+ defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do
+ Map.put(
+ data,
+ :block_expires_at,
+ UserRelationship.get_block_expire_date(target, user)
+ )
+ end
+
+ defp maybe_put_block_expires_at(data, _, _, _), do: data
+
defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in([:pleroma, :birthday], user.birthday)
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 913684928..1b6f26af7 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -90,6 +90,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
}
end
+ def render("translation_languages.json", _) do
+ with true <- Pleroma.Language.Translation.configured?(),
+ {:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do
+ languages
+ else
+ _ -> %{}
+ end
+ end
+
defp common_information(instance) do
%{
languages: Keyword.get(instance, :languages, ["en"]),
@@ -145,7 +154,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
end,
"pleroma:get:main/ostatus",
"pleroma:group_actors",
- "pleroma:bookmark_folders"
+ "pleroma:bookmark_folders",
+ if Pleroma.Language.LanguageDetector.configured?() do
+ "pleroma:language_detection"
+ end,
+ "pleroma:block_expiration"
]
|> Enum.filter(& &1)
end
@@ -243,11 +256,27 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
},
vapid: %{
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
- }
+ },
+ translation: %{enabled: Pleroma.Language.Translation.configured?()}
})
end
defp pleroma_configuration(instance) do
+ base_urls = %{}
+
+ base_urls =
+ if Config.get([:media_proxy, :enabled]) do
+ Map.put(base_urls, :media_proxy, Config.get([:media_proxy, :base_url]))
+ else
+ base_urls
+ end
+
+ base_urls =
+ case Config.get([Pleroma.Upload, :base_url]) do
+ nil -> base_urls
+ url -> Map.put(base_urls, :upload, url)
+ end
+
%{
metadata: %{
account_activation_required: Keyword.get(instance, :account_activation_required),
@@ -256,7 +285,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
fields_limits: fields_limits(),
post_formats: Config.get([:instance, :allowed_post_formats]),
birthday_required: Config.get([:instance, :birthday_required]),
- birthday_min_age: Config.get([:instance, :birthday_min_age])
+ birthday_min_age: Config.get([:instance, :birthday_min_age]),
+ translation: supported_languages(),
+ base_urls: base_urls,
+ markup: markup()
},
stats: %{mau: Pleroma.User.active_user_count()},
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
@@ -282,4 +314,37 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
})
})
end
+
+ defp supported_languages do
+ enabled = Pleroma.Language.Translation.configured?()
+
+ source_languages =
+ with true <- enabled,
+ {:ok, languages} <- Pleroma.Language.Translation.supported_languages(:source) do
+ languages
+ else
+ _ -> nil
+ end
+
+ target_languages =
+ with true <- enabled,
+ {:ok, languages} <- Pleroma.Language.Translation.supported_languages(:target) do
+ languages
+ else
+ _ -> nil
+ end
+
+ %{
+ source_languages: source_languages,
+ target_languages: target_languages
+ }
+ end
+
+ defp markup do
+ %{
+ allow_inline_images: Config.get([:markup, :allow_inline_images]),
+ allow_headings: Config.get([:markup, :allow_headings]),
+ allow_tables: Config.get([:markup, :allow_tables])
+ }
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 3bf870c24..4b5ac9c3b 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -227,7 +227,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
mentions: mentions,
tags: reblogged[:tags] || [],
application: build_application(object.data["generator"]),
- language: nil,
+ language: get_language(object),
emojis: [],
pleroma: %{
local: activity.local,
@@ -445,7 +445,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
mentions: mentions,
tags: build_tags(tags),
application: build_application(object.data["generator"]),
- language: nil,
+ language: get_language(object),
emojis: build_emojis(object.data["emoji"]),
pleroma: %{
local: activity.local,
@@ -681,6 +681,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
+ def render("translation.json", %{
+ content: content,
+ detected_source_language: detected_source_language,
+ provider: provider
+ }) do
+ %{content: content, detected_source_language: detected_source_language, provider: provider}
+ end
+
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity, fetch: false)
@@ -829,6 +837,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
Utils.get_content_type(nil)
end
+ defp get_language(%{data: %{"language" => "und"}}), do: nil
+
+ defp get_language(object), do: object.data["language"]
+
defp proxied_url(url, page_url_data) do
if is_binary(url) do
build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()
diff --git a/lib/pleroma/web/mastodon_api/views/tag_view.ex b/lib/pleroma/web/mastodon_api/views/tag_view.ex
new file mode 100644
index 000000000..e24d423c2
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/tag_view.ex
@@ -0,0 +1,25 @@
+defmodule Pleroma.Web.MastodonAPI.TagView do
+ use Pleroma.Web, :view
+ alias Pleroma.User
+ alias Pleroma.Web.Router.Helpers
+
+ def render("index.json", %{tags: tags, for_user: user}) do
+ safe_render_many(tags, __MODULE__, "show.json", %{for_user: user})
+ end
+
+ def render("show.json", %{tag: tag, for_user: user}) do
+ following =
+ with %User{} <- user do
+ User.following_hashtag?(user, tag)
+ else
+ _ -> false
+ end
+
+ %{
+ name: tag.name,
+ url: Helpers.tag_feed_url(Pleroma.Web.Endpoint, :feed, tag.name),
+ history: [],
+ following: following
+ }
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 730295a4c..3ed1cdd6c 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
# This only prepares the connection and is not in the process yet
@impl Phoenix.Socket.Transport
def connect(%{params: params} = transport_info) do
- with access_token <- Map.get(params, "access_token"),
+ with access_token <- find_access_token(transport_info),
{:ok, user, oauth_token} <- authenticate_request(access_token),
{:ok, topic} <-
Streamer.get_topic(params["stream"], user, oauth_token, params) do
@@ -244,4 +244,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
def handle_error(conn, _reason) do
Plug.Conn.send_resp(conn, 404, "Not Found")
end
+
+ defp find_access_token(%{
+ connect_info: %{sec_websocket_protocol: [token]}
+ }),
+ do: token
+
+ defp find_access_token(%{params: %{"access_token" => token}}), do: token
+
+ defp find_access_token(_), do: nil
end
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 0b446e0a6..a0aafc32e 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -71,11 +71,15 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
drop_static_param_and_redirect(conn)
content_type == "image/gif" ->
- redirect(conn, external: media_proxy_url)
+ conn
+ |> put_status(301)
+ |> redirect(external: media_proxy_url)
min_content_length_for_preview() > 0 and content_length > 0 and
content_length < min_content_length_for_preview() ->
- redirect(conn, external: media_proxy_url)
+ conn
+ |> put_status(301)
+ |> redirect(external: media_proxy_url)
true ->
handle_preview(content_type, conn, media_proxy_url)
diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex
index 59d018730..4ee7c41ec 100644
--- a/lib/pleroma/web/metadata.ex
+++ b/lib/pleroma/web/metadata.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.Metadata do
def build_tags(params) do
providers = [
+ Pleroma.Web.Metadata.Providers.ActivityPub,
Pleroma.Web.Metadata.Providers.RelMe,
Pleroma.Web.Metadata.Providers.RestrictIndexing
| activated_providers()
diff --git a/lib/pleroma/web/metadata/providers/activity_pub.ex b/lib/pleroma/web/metadata/providers/activity_pub.ex
new file mode 100644
index 000000000..bd9f92332
--- /dev/null
+++ b/lib/pleroma/web/metadata/providers/activity_pub.ex
@@ -0,0 +1,22 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.ActivityPub do
+ alias Pleroma.Web.Metadata.Providers.Provider
+
+ @behaviour Provider
+
+ @impl Provider
+ def build_tags(%{object: %{data: %{"id" => object_id}}}) do
+ [{:link, [rel: "alternate", type: "application/activity+json", href: object_id], []}]
+ end
+
+ @impl Provider
+ def build_tags(%{user: user}) do
+ [{:link, [rel: "alternate", type: "application/activity+json", href: user.ap_id], []}]
+ end
+
+ @impl Provider
+ def build_tags(_), do: []
+end
diff --git a/lib/pleroma/web/metadata/providers/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex
index e97d6a54f..5a0f2338e 100644
--- a/lib/pleroma/web/metadata/providers/feed.ex
+++ b/lib/pleroma/web/metadata/providers/feed.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do
@behaviour Provider
@impl Provider
- def build_tags(%{user: user}) do
+ def build_tags(%{user: %{local: true} = user}) do
[
{:link,
[
@@ -20,4 +20,7 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do
], []}
]
end
+
+ @impl Provider
+ def build_tags(_), do: []
end
diff --git a/lib/pleroma/web/metadata/providers/open_graph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex
index 97d3865ed..604434df2 100644
--- a/lib/pleroma/web/metadata/providers/open_graph.ex
+++ b/lib/pleroma/web/metadata/providers/open_graph.ex
@@ -67,6 +67,9 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
end
end
+ @impl Provider
+ def build_tags(_), do: []
+
defp build_attachments(%{data: %{"attachment" => attachments}}) do
Enum.reduce(attachments, [], fn attachment, acc ->
rendered_tags =
@@ -75,10 +78,10 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
# object when a Video or GIF is attached it will display that in Whatsapp Rich Preview.
case Utils.fetch_media_type(@media_types, url["mediaType"]) do
"audio" ->
- [
- {:meta, [property: "og:audio", content: MediaProxy.url(url["href"])], []}
- | acc
- ]
+ acc ++
+ [
+ {:meta, [property: "og:audio", content: MediaProxy.url(url["href"])], []}
+ ]
# Not using preview_url for this. It saves bandwidth, but the image dimensions will
# be wrong. We generate it on the fly and have no way to capture or analyze the
@@ -86,18 +89,18 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
# in timelines too, but you can get clever with the aspect ratio metadata as a
# workaround.
"image" ->
- [
- {:meta, [property: "og:image", content: MediaProxy.url(url["href"])], []},
- {:meta, [property: "og:image:alt", content: attachment["name"]], []}
- | acc
- ]
+ (acc ++
+ [
+ {:meta, [property: "og:image", content: MediaProxy.url(url["href"])], []},
+ {:meta, [property: "og:image:alt", content: attachment["name"]], []}
+ ])
|> maybe_add_dimensions(url)
"video" ->
- [
- {:meta, [property: "og:video", content: MediaProxy.url(url["href"])], []}
- | acc
- ]
+ (acc ++
+ [
+ {:meta, [property: "og:video", content: MediaProxy.url(url["href"])], []}
+ ])
|> maybe_add_dimensions(url)
|> maybe_add_video_thumbnail(url)
diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex
index eabd8cb00..39aa71f06 100644
--- a/lib/pleroma/web/metadata/providers/rel_me.ex
+++ b/lib/pleroma/web/metadata/providers/rel_me.ex
@@ -20,6 +20,9 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do
end)
end
+ @impl Provider
+ def build_tags(_), do: []
+
defp append_fields_tag(bio, fields) do
fields
|> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end)
diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex
index 426022c65..212fa85ed 100644
--- a/lib/pleroma/web/metadata/providers/twitter_card.ex
+++ b/lib/pleroma/web/metadata/providers/twitter_card.ex
@@ -44,6 +44,9 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
end
end
+ @impl Provider
+ def build_tags(_), do: []
+
defp title_tag(user) do
{:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}
end
@@ -58,13 +61,13 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
Enum.reduce(attachment["url"], [], fn url, acc ->
case Utils.fetch_media_type(@media_types, url["mediaType"]) do
"audio" ->
- [
- {:meta, [name: "twitter:card", content: "player"], []},
- {:meta, [name: "twitter:player:width", content: "480"], []},
- {:meta, [name: "twitter:player:height", content: "80"], []},
- {:meta, [name: "twitter:player", content: player_url(id)], []}
- | acc
- ]
+ acc ++
+ [
+ {:meta, [name: "twitter:card", content: "player"], []},
+ {:meta, [name: "twitter:player:width", content: "480"], []},
+ {:meta, [name: "twitter:player:height", content: "80"], []},
+ {:meta, [name: "twitter:player", content: player_url(id)], []}
+ ]
# Not using preview_url for this. It saves bandwidth, but the image dimensions will
# be wrong. We generate it on the fly and have no way to capture or analyze the
@@ -72,16 +75,16 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
# in timelines too, but you can get clever with the aspect ratio metadata as a
# workaround.
"image" ->
- [
- {:meta, [name: "twitter:card", content: "summary_large_image"], []},
- {:meta,
+ (acc ++
[
- name: "twitter:image",
- content: MediaProxy.url(url["href"])
- ], []},
- {:meta, [name: "twitter:image:alt", content: truncate(attachment["name"])], []}
- | acc
- ]
+ {:meta, [name: "twitter:card", content: "summary_large_image"], []},
+ {:meta,
+ [
+ name: "twitter:image",
+ content: MediaProxy.url(url["href"])
+ ], []},
+ {:meta, [name: "twitter:image:alt", content: truncate(attachment["name"])], []}
+ ])
|> maybe_add_dimensions(url)
"video" ->
@@ -89,17 +92,17 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
height = url["height"] || 480
width = url["width"] || 480
- [
- {:meta, [name: "twitter:card", content: "player"], []},
- {:meta, [name: "twitter:player", content: player_url(id)], []},
- {:meta, [name: "twitter:player:width", content: "#{width}"], []},
- {:meta, [name: "twitter:player:height", content: "#{height}"], []},
- {:meta, [name: "twitter:player:stream", content: MediaProxy.url(url["href"])],
- []},
- {:meta, [name: "twitter:player:stream:content_type", content: url["mediaType"]],
- []}
- | acc
- ]
+ acc ++
+ [
+ {:meta, [name: "twitter:card", content: "player"], []},
+ {:meta, [name: "twitter:player", content: player_url(id)], []},
+ {:meta, [name: "twitter:player:width", content: "#{width}"], []},
+ {:meta, [name: "twitter:player:height", content: "#{height}"], []},
+ {:meta, [name: "twitter:player:stream", content: MediaProxy.url(url["href"])],
+ []},
+ {:meta, [name: "twitter:player:stream:content_type", content: url["mediaType"]],
+ []}
+ ]
_ ->
acc
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
index 32360d2a2..8c5e4c06a 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -16,6 +16,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
:import_from_filesystem,
:remote,
:download,
+ :download_zip,
:create,
:update,
:delete
@@ -113,6 +114,27 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
+ def download_zip(
+ %{private: %{open_api_spex: %{body_params: params}}} = conn,
+ _
+ ) do
+ name = Map.get(params, :name)
+
+ with :ok <- Pack.download_zip(name, params) do
+ json(conn, "ok")
+ else
+ {:error, error} when is_binary(error) ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: error})
+
+ {:error, _} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Could not process pack"})
+ end
+ end
+
def download(
%{private: %{open_api_spex: %{body_params: %{url: url, name: name} = params}}} = conn,
_
diff --git a/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex
index 6257e3153..85cfe9f00 100644
--- a/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.PleromaAPI.InstancesController do
def show(conn, _params) do
unreachable =
- Instances.get_consistently_unreachable()
+ Instances.get_unreachable()
|> Map.new(fn {host, date} -> {host, to_string(date)} end)
json(conn, %{"unreachable" => unreachable})
diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
index bf6dc500c..5f5f7643f 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -24,6 +24,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
+ params =
+ params
+ |> Map.put_new(:external_link, Map.get(params, :externalLink))
+
with {:ok, activity} <- CommonAPI.listen(user, params) do
render(conn, "show.json", activity: activity, for: user)
else
diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
index edf0a2390..51828ad97 100644
--- a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
@@ -27,8 +27,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
title: object.data["title"] |> HTML.strip_tags(),
artist: object.data["artist"] |> HTML.strip_tags(),
album: object.data["album"] |> HTML.strip_tags(),
- externalLink: object.data["externalLink"],
- length: object.data["length"]
+ external_link: object.data["externalLink"],
+ length: object.data["length"],
+ # DEPRECATED
+ externalLink: object.data["externalLink"]
}
end
diff --git a/lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex b/lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex
new file mode 100644
index 000000000..6807673f9
--- /dev/null
+++ b/lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex
@@ -0,0 +1,34 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.APClientApiEnabledPlug do
+ import Plug.Conn
+ import Phoenix.Controller, only: [text: 2]
+
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+ @enabled_path [:activitypub, :client_api_enabled]
+
+ def init(options \\ []), do: Map.new(options)
+
+ def call(conn, %{allow_server: true}) do
+ if @config_impl.get(@enabled_path, false) do
+ conn
+ else
+ conn
+ |> assign(:user, nil)
+ |> assign(:token, nil)
+ end
+ end
+
+ def call(conn, _) do
+ if @config_impl.get(@enabled_path, false) do
+ conn
+ else
+ conn
+ |> put_status(:forbidden)
+ |> text("C2S not enabled")
+ |> halt()
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
index 67974599a..2e16212ce 100644
--- a/lib/pleroma/web/plugs/http_signature_plug.ex
+++ b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -19,8 +19,16 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
options
end
- def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
- conn
+ def call(%{assigns: %{valid_signature: true}} = conn, _opts), do: conn
+
+ # skip for C2S requests from authenticated users
+ def call(%{assigns: %{user: %Pleroma.User{}}} = conn, _opts) do
+ if get_format(conn) in ["json", "activity+json"] do
+ # ensure access token is provided for 2FA
+ Pleroma.Web.Plugs.EnsureAuthenticatedPlug.call(conn, %{})
+ else
+ conn
+ end
end
def call(conn, _opts) do
diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex
index 75bfdd65b..f82b9a098 100644
--- a/lib/pleroma/web/plugs/instance_static.ex
+++ b/lib/pleroma/web/plugs/instance_static.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.Plugs.InstanceStatic do
require Pleroma.Constants
+ import Plug.Conn, only: [put_resp_header: 3]
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration.
@@ -44,10 +45,31 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
end
defp call_static(conn, opts, from) do
+ # Prevent content-type spoofing by setting content_types: false
opts =
opts
|> Map.put(:from, from)
+ |> Map.put(:content_types, false)
+ conn = set_content_type(conn, conn.request_path)
+
+ # Call Plug.Static with our sanitized content-type
Plug.Static.call(conn, opts)
end
+
+ defp set_content_type(conn, "/emoji/" <> filepath) do
+ real_mime = MIME.from_path(filepath)
+
+ clean_mime =
+ Pleroma.Web.Plugs.Utils.get_safe_mime_type(%{allowed_mime_types: ["image"]}, real_mime)
+
+ put_resp_header(conn, "content-type", clean_mime)
+ end
+
+ defp set_content_type(conn, filepath) do
+ real_mime = MIME.from_path(filepath)
+ put_resp_header(conn, "content-type", real_mime)
+ end
end
+
+# I think this needs to be uncleaned except for emoji.
diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
index f1076da1b..abacf965b 100644
--- a/lib/pleroma/web/plugs/uploaded_media.ex
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
require Logger
alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.Plugs.Utils
@behaviour Plug
# no slashes
@@ -28,7 +29,9 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
|> Keyword.put(:at, "/__unconfigured_media_plug")
|> Plug.Static.init()
- %{static_plug_opts: static_plug_opts}
+ allowed_mime_types = Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types])
+
+ %{static_plug_opts: static_plug_opts, allowed_mime_types: allowed_mime_types}
end
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
@@ -69,13 +72,23 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
defp media_is_banned(_, _), do: false
+ defp set_content_type(conn, opts, filepath) do
+ real_mime = MIME.from_path(filepath)
+ clean_mime = Utils.get_safe_mime_type(opts, real_mime)
+ put_resp_header(conn, "content-type", clean_mime)
+ end
+
defp get_media(conn, {:static_dir, directory}, _, opts) do
static_opts =
Map.get(opts, :static_plug_opts)
|> Map.put(:at, [@path])
|> Map.put(:from, directory)
+ |> Map.put(:content_types, false)
- conn = Plug.Static.call(conn, static_opts)
+ conn =
+ conn
+ |> set_content_type(opts, conn.request_path)
+ |> Plug.Static.call(static_opts)
if conn.halted do
conn
diff --git a/lib/pleroma/web/plugs/utils.ex b/lib/pleroma/web/plugs/utils.ex
new file mode 100644
index 000000000..05e0fbe84
--- /dev/null
+++ b/lib/pleroma/web/plugs/utils.ex
@@ -0,0 +1,14 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.Utils do
+ @moduledoc """
+ Some helper functions shared across several plugs
+ """
+
+ def get_safe_mime_type(%{allowed_mime_types: allowed_mime_types} = _opts, mime) do
+ [maintype | _] = String.split(mime, "/", parts: 2)
+ if maintype in allowed_mime_types, do: mime, else: "application/octet-stream"
+ end
+end
diff --git a/lib/pleroma/web/push.ex b/lib/pleroma/web/push.ex
index 6d777142e..77f77f88e 100644
--- a/lib/pleroma/web/push.ex
+++ b/lib/pleroma/web/push.ex
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.Push do
end
def vapid_config do
- Application.get_env(:web_push_encryption, :vapid_details, nil)
+ Application.get_env(:web_push_encryption, :vapid_details, [])
end
def enabled, do: match?([subject: _, public_key: _, private_key: _], vapid_config())
diff --git a/lib/pleroma/web/rich_media/card.ex b/lib/pleroma/web/rich_media/card.ex
index abad4957e..6b4bb9555 100644
--- a/lib/pleroma/web/rich_media/card.ex
+++ b/lib/pleroma/web/rich_media/card.ex
@@ -54,7 +54,10 @@ defmodule Pleroma.Web.RichMedia.Card do
@spec get_by_url(String.t() | nil) :: t() | nil | :error
def get_by_url(url) when is_binary(url) do
- if @config_impl.get([:rich_media, :enabled]) do
+ host = URI.parse(url).host
+
+ with true <- @config_impl.get([:rich_media, :enabled]),
+ true <- host not in @config_impl.get([:rich_media, :ignore_hosts], []) do
url_hash = url_to_hash(url)
@cachex.fetch!(:rich_media_cache, url_hash, fn _ ->
@@ -69,7 +72,7 @@ defmodule Pleroma.Web.RichMedia.Card do
end
end)
else
- :error
+ false -> :error
end
end
@@ -77,7 +80,10 @@ defmodule Pleroma.Web.RichMedia.Card do
@spec get_or_backfill_by_url(String.t(), keyword()) :: t() | nil
def get_or_backfill_by_url(url, opts \\ []) do
- if @config_impl.get([:rich_media, :enabled]) do
+ host = URI.parse(url).host
+
+ with true <- @config_impl.get([:rich_media, :enabled]),
+ true <- host not in @config_impl.get([:rich_media, :ignore_hosts], []) do
case get_by_url(url) do
%__MODULE__{} = card ->
card
@@ -94,7 +100,7 @@ defmodule Pleroma.Web.RichMedia.Card do
nil
end
else
- nil
+ false -> nil
end
end
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index a3a522d7a..9c8ec7a9f 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.RichMedia.Parser do
alias Pleroma.Web.RichMedia.Helpers
+ import Pleroma.Web.Metadata.Utils, only: [scrub_html_and_truncate: 2]
require Logger
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
@@ -63,8 +64,20 @@ defmodule Pleroma.Web.RichMedia.Parser do
not match?({:ok, _}, Jason.encode(%{key => val}))
end)
|> Map.new()
+ |> truncate_title()
+ |> truncate_desc()
end
+ defp truncate_title(%{"title" => title} = data) when is_binary(title),
+ do: %{data | "title" => scrub_html_and_truncate(title, 120)}
+
+ defp truncate_title(data), do: data
+
+ defp truncate_desc(%{"description" => desc} = data) when is_binary(desc),
+ do: %{data | "description" => scrub_html_and_truncate(desc, 200)}
+
+ defp truncate_desc(data), do: data
+
@spec validate_page_url(URI.t() | binary()) :: :ok | :error
defp validate_page_url(page_url) when is_binary(page_url) do
validate_tld = @config_impl.get([Pleroma.Formatter, :validate_tld])
diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
index 320a5f515..c42e2c96b 100644
--- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
+++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do
|> Enum.reduce(data, fn el, acc ->
attributes = normalize_attributes(el, prefix, key_name, value_name)
- Map.merge(acc, attributes)
+ Map.merge(attributes, acc)
end)
|> maybe_put_title(html)
end
diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
index cc653729d..6f6f8b2ae 100644
--- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex
+++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
@@ -11,5 +11,16 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do
|> MetaTagsParser.parse(html, "og", "property")
|> MetaTagsParser.parse(html, "twitter", "name")
|> MetaTagsParser.parse(html, "twitter", "property")
+ |> filter_tags()
+ end
+
+ defp filter_tags(tags) do
+ Map.filter(tags, fn {k, _v} ->
+ cond do
+ k in ["card", "description", "image", "title", "ttl", "type", "url"] -> true
+ String.starts_with?(k, "image:") -> true
+ true -> false
+ end
+ end)
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 0423ca9e2..cd9cfd3ed 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -189,7 +189,7 @@ defmodule Pleroma.Web.Router do
end
pipeline :well_known do
- plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml"])
+ plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml", "html"])
end
pipeline :config do
@@ -466,6 +466,7 @@ defmodule Pleroma.Web.Router do
get("/import", EmojiPackController, :import_from_filesystem)
get("/remote", EmojiPackController, :remote)
post("/download", EmojiPackController, :download)
+ post("/download_zip", EmojiPackController, :download_zip)
post("/files", EmojiFileController, :create)
patch("/files", EmojiFileController, :update)
@@ -740,6 +741,7 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
+ post("/statuses/:id/translate", StatusController, :translate)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :show)
@@ -755,6 +757,11 @@ defmodule Pleroma.Web.Router do
get("/announcements", AnnouncementController, :index)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
+
+ get("/tags/:id", TagController, :show)
+ post("/tags/:id/follow", TagController, :follow)
+ post("/tags/:id/unfollow", TagController, :unfollow)
+ get("/followed_tags", TagController, :show_followed)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
@@ -782,6 +789,7 @@ defmodule Pleroma.Web.Router do
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
get("/instance/rules", InstanceController, :rules)
+ get("/instance/translation_languages", InstanceController, :translation_languages)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
@@ -902,22 +910,37 @@ defmodule Pleroma.Web.Router do
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
plug(:ap_service_actor)
+ plug(Pleroma.Web.Plugs.APClientApiEnabledPlug)
plug(:fetch_session)
plug(:authenticate)
plug(:after_auth)
end
+ # AP interactions used by both S2S and C2S
+ pipeline :activitypub_server_or_client do
+ plug(:ap_service_actor)
+ plug(:fetch_session)
+ plug(:authenticate)
+ plug(Pleroma.Web.Plugs.APClientApiEnabledPlug, allow_server: true)
+ plug(:after_auth)
+ plug(:http_signature)
+ end
+
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
- get("/users/:nickname/outbox", ActivityPubController, :outbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
+ end
+
+ scope "/", Pleroma.Web.ActivityPub do
+ pipe_through([:activitypub_server_or_client])
+
+ get("/users/:nickname/outbox", ActivityPubController, :outbox)
- # The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 76dc0f42d..cc149e04c 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.StreamerView
+ require Pleroma.Constants
@registry Pleroma.Web.StreamerRegistry
@@ -305,7 +306,17 @@ defmodule Pleroma.Web.Streamer do
User.get_recipients_from_activity(item)
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
- Enum.each(recipient_topics, fn topic ->
+ hashtag_recipients =
+ if Pleroma.Constants.as_public() in item.recipients do
+ Pleroma.Hashtag.get_recipients_for_activity(item)
+ |> Enum.map(fn id -> "user:#{id}" end)
+ else
+ []
+ end
+
+ all_recipients = Enum.uniq(recipient_topics ++ hashtag_recipients)
+
+ Enum.each(all_recipients, fn topic ->
push_to_socket(topic, item)
end)
end
diff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex
index 1efc76e1a..d2918bc6f 100644
--- a/lib/pleroma/web/templates/email/digest.html.eex
+++ b/lib/pleroma/web/templates/email/digest.html.eex
@@ -231,8 +231,8 @@
<%= for %{data: mention, object: object, from: from} <- @mentions do %>
- <%# mention START %>
- <%# user card START %>
+ <% # mention START %>
+ <% # user card START %>
- <%# user card END %>
+ <% # user card END %>
- <%# mention END %>
+ <% # mention END %>
<% end %>
<%= if @followers != [] do %>
- <%# new followers header START %>
+ <% # new followers header START %>
- <%# new followers header END %>
+ <% # new followers header END %>
<%= for %{data: follow, from: from} <- @followers do %>
- <%# user card START %>
+ <% # user card START %>
- <%# user card END %>
+ <% # user card END %>
<% end %>
<% end %>
- <%# divider start %>
+ <% # divider start %>
- <%# divider end %>
+ <% # divider end %>
diff --git a/lib/pleroma/web/templates/email/new_users_digest.html.eex b/lib/pleroma/web/templates/email/new_users_digest.html.eex
index 40d9b8381..78b8ac4f9 100644
--- a/lib/pleroma/web/templates/email/new_users_digest.html.eex
+++ b/lib/pleroma/web/templates/email/new_users_digest.html.eex
@@ -1,5 +1,5 @@
<%= for {user, total_statuses, latest_status} <- @users_and_statuses do %>
- <%# user card START %>
+ <% # user card START %>
- <%# user card END %>
+ <% # user card END %>
<%= if latest_status do %>
@@ -104,7 +104,7 @@
<% end %>
- <%# divider start %>
+ <% # divider start %>
- <%# divider end %>
- <%# user card END %>
+ <% # divider end %>
+ <% # user card END %>
<% end %>
diff --git a/lib/pleroma/web/templates/layout/email_styled.html.eex b/lib/pleroma/web/templates/layout/email_styled.html.eex
index 82cabd889..a1ed4ece3 100644
--- a/lib/pleroma/web/templates/layout/email_styled.html.eex
+++ b/lib/pleroma/web/templates/layout/email_styled.html.eex
@@ -111,7 +111,7 @@
- <%# header %>
+ <% # header %>
@@ -145,7 +145,7 @@
- <%# title %>
+ <% # title %>
<%= if @title do %>
<%= for scope <- @available_scopes do %>
- <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
+ <% # Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
<%= if scope in @scopes do %>
<%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %>
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 6805233df..aeafa195d 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Healthcheck
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.WebFinger
@@ -195,19 +196,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
_
) do
- case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
- {:ok, user} ->
- with {:ok, _user} <-
- User.reset_password(user, %{
- password: body_params.new_password,
- password_confirmation: body_params.new_password_confirmation
- }) do
- json(conn, %{status: "success"})
- else
- {:error, changeset} ->
- {_, {error, _}} = Enum.at(changeset.errors, 0)
- json(conn, %{error: "New password #{error}."})
- end
+ with {:ok, %User{}} <-
+ Authenticator.change_password(
+ user,
+ body_params.password,
+ body_params.new_password,
+ body_params.new_password_confirmation
+ ) do
+ json(conn, %{status: "success"})
+ else
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {_, {error, _}} = Enum.at(changeset.errors, 0)
+ json(conn, %{error: "New password #{error}."})
+
+ {:error, :password_confirmation} ->
+ json(conn, %{error: "New password does not match confirmation."})
{:error, msg} ->
json(conn, %{error: msg})
diff --git a/lib/pleroma/web/twitter_api/views/token_view.ex b/lib/pleroma/web/twitter_api/views/token_view.ex
index 2e492c13f..36776ce3b 100644
--- a/lib/pleroma/web/twitter_api/views/token_view.ex
+++ b/lib/pleroma/web/twitter_api/views/token_view.ex
@@ -15,7 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.TokenView do
%{
id: token_entry.id,
valid_until: token_entry.valid_until,
- app_name: token_entry.app.client_name
+ app_name: token_entry.app.client_name,
+ scopes: token_entry.scopes
}
end
end
diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex
index e653b3338..a53d58caa 100644
--- a/lib/pleroma/web/web_finger.ex
+++ b/lib/pleroma/web/web_finger.ex
@@ -35,9 +35,9 @@ defmodule Pleroma.Web.WebFinger do
regex =
if webfinger_domain = Pleroma.Config.get([__MODULE__, :domain]) do
- ~r/(acct:)?(?
[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})/
+ ~r/(acct:)?(?[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})$/
else
- ~r/(acct:)?(?[a-z0-9A-Z_\.-]+)@#{host}/
+ ~r/(acct:)?(?[a-z0-9A-Z_\.-]+)@#{host}$/
end
with %{"username" => username} <- Regex.named_captures(regex, resource),
diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex
index 021df9bc5..8a291e28e 100644
--- a/lib/pleroma/web/web_finger/web_finger_controller.ex
+++ b/lib/pleroma/web/web_finger/web_finger_controller.ex
@@ -41,5 +41,10 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
end
end
+ # Default to JSON when no format is specified or format is not recognized
+ def webfinger(%{assigns: %{format: _format}} = conn, %{"resource" => _resource} = params) do
+ webfinger(put_in(conn.assigns.format, "json"), params)
+ end
+
def webfinger(conn, _params), do: send_resp(conn, 400, "Bad Request")
end
diff --git a/lib/pleroma/workers/delete_worker.ex b/lib/pleroma/workers/delete_worker.ex
index 6a1c7bb38..b83185fff 100644
--- a/lib/pleroma/workers/delete_worker.ex
+++ b/lib/pleroma/workers/delete_worker.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.DeleteWorker do
- alias Pleroma.Instances.Instance
alias Pleroma.User
use Oban.Worker, queue: :slow
@@ -15,7 +14,27 @@ defmodule Pleroma.Workers.DeleteWorker do
end
def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do
- Instance.perform(:delete_instance, host)
+ # Schedule the per-user deletion jobs
+ Pleroma.Repo.transaction(fn ->
+ User.Query.build(%{nickname: "@#{host}"})
+ |> Pleroma.Repo.all()
+ |> Enum.each(fn user ->
+ %{"op" => "delete_user", "user_id" => user.id}
+ |> __MODULE__.new()
+ |> Oban.insert()
+ end)
+
+ # Delete the instance from the Instances table
+ case Pleroma.Repo.get_by(Pleroma.Instances.Instance, host: host) do
+ nil -> :ok
+ instance -> Pleroma.Repo.delete(instance)
+ end
+
+ # Delete any pending ReachabilityWorker jobs for this domain
+ Pleroma.Workers.ReachabilityWorker.delete_jobs_for_host(host)
+
+ :ok
+ end)
end
@impl true
diff --git a/lib/pleroma/workers/mute_expire_worker.ex b/lib/pleroma/workers/mute_expire_worker.ex
index 8356a775d..9a04fc486 100644
--- a/lib/pleroma/workers/mute_expire_worker.ex
+++ b/lib/pleroma/workers/mute_expire_worker.ex
@@ -5,9 +5,13 @@
defmodule Pleroma.Workers.MuteExpireWorker do
use Oban.Worker, queue: :background
+ alias Pleroma.User
+
@impl true
- def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do
- Pleroma.User.unmute(muter_id, mutee_id)
+ def perform(%Job{
+ args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}
+ }) do
+ User.unmute(muter_id, mutee_id)
:ok
end
@@ -18,6 +22,17 @@ defmodule Pleroma.Workers.MuteExpireWorker do
:ok
end
+ def perform(%Job{
+ args: %{"op" => "unblock_user", "blocker_id" => blocker_id, "blocked_id" => blocked_id}
+ }) do
+ Pleroma.Web.CommonAPI.unblock(
+ User.get_cached_by_id(blocked_id),
+ User.get_cached_by_id(blocker_id)
+ )
+
+ :ok
+ end
+
@impl true
def timeout(_job), do: :timer.seconds(5)
end
diff --git a/lib/pleroma/workers/poll_worker.ex b/lib/pleroma/workers/poll_worker.ex
index d263aa1b9..a9afe9d63 100644
--- a/lib/pleroma/workers/poll_worker.ex
+++ b/lib/pleroma/workers/poll_worker.ex
@@ -11,27 +11,46 @@ defmodule Pleroma.Workers.PollWorker do
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
+ alias Pleroma.Object.Fetcher
+
+ @stream_out_impl Pleroma.Config.get(
+ [__MODULE__, :stream_out],
+ Pleroma.Web.ActivityPub.ActivityPub
+ )
@impl true
def perform(%Job{args: %{"op" => "poll_end", "activity_id" => activity_id}}) do
- with %Activity{} = activity <- find_poll_activity(activity_id),
+ with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)},
{:ok, notifications} <- Notification.create_poll_notifications(activity) do
+ unless activity.local do
+ # Schedule a final refresh
+ __MODULE__.new(%{"op" => "refresh", "activity_id" => activity_id})
+ |> Oban.insert()
+ end
+
Notification.stream(notifications)
else
- {:error, :poll_activity_not_found} = e -> {:cancel, e}
+ {:activity, nil} -> {:cancel, :poll_activity_not_found}
e -> {:error, e}
end
end
+ def perform(%Job{args: %{"op" => "refresh", "activity_id" => activity_id}}) do
+ with {_, %Activity{object: object}} <-
+ {:activity, Activity.get_by_id_with_object(activity_id)},
+ {_, {:ok, _object}} <- {:refetch, Fetcher.refetch_object(object)} do
+ stream_update(activity_id)
+
+ :ok
+ else
+ {:activity, nil} -> {:cancel, :poll_activity_not_found}
+ {:refetch, _} = e -> {:cancel, e}
+ end
+ end
+
@impl true
def timeout(_job), do: :timer.seconds(5)
- defp find_poll_activity(activity_id) do
- with nil <- Activity.get_by_id(activity_id) do
- {:error, :poll_activity_not_found}
- end
- end
-
def schedule_poll_end(%Activity{data: %{"type" => "Create"}, id: activity_id} = activity) do
with %Object{data: %{"type" => "Question", "closed" => closed}} when is_binary(closed) <-
Object.normalize(activity),
@@ -49,4 +68,10 @@ defmodule Pleroma.Workers.PollWorker do
end
def schedule_poll_end(activity), do: {:error, activity}
+
+ defp stream_update(activity_id) do
+ Activity.get_by_id(activity_id)
+ |> Activity.normalize()
+ |> @stream_out_impl.stream_out()
+ end
end
diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex
index 7d9b022de..f799af77a 100644
--- a/lib/pleroma/workers/publisher_worker.ex
+++ b/lib/pleroma/workers/publisher_worker.ex
@@ -4,9 +4,10 @@
defmodule Pleroma.Workers.PublisherWorker do
alias Pleroma.Activity
+ alias Pleroma.Instances
alias Pleroma.Web.Federator
- use Oban.Worker, queue: :federator_outgoing, max_attempts: 5
+ use Oban.Worker, queue: :federator_outgoing, max_attempts: 13
@impl true
def perform(%Job{args: %{"op" => "publish", "activity_id" => activity_id}}) do
@@ -14,9 +15,30 @@ defmodule Pleroma.Workers.PublisherWorker do
Federator.perform(:publish, activity)
end
- def perform(%Job{args: %{"op" => "publish_one", "params" => params}}) do
+ def perform(%Job{args: %{"op" => "publish_one", "params" => params}} = job) do
params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end)
- Federator.perform(:publish_one, params)
+
+ # Cancel / skip the job if this server believed to be unreachable now
+ if not Instances.reachable?(params.inbox) do
+ {:cancel, :unreachable}
+ else
+ case Federator.perform(:publish_one, params) do
+ {:ok, _} ->
+ :ok
+
+ {:error, _} = error ->
+ # Only mark as unreachable on final failure
+ if job.attempt == job.max_attempts do
+ Instances.set_unreachable(params.inbox)
+ end
+
+ error
+
+ error ->
+ # Unexpected error, may have been client side
+ error
+ end
+ end
end
@impl true
diff --git a/lib/pleroma/workers/reachability_worker.ex b/lib/pleroma/workers/reachability_worker.ex
new file mode 100644
index 000000000..41981a2e4
--- /dev/null
+++ b/lib/pleroma/workers/reachability_worker.ex
@@ -0,0 +1,116 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.ReachabilityWorker do
+ use Oban.Worker,
+ queue: :background,
+ max_attempts: 1,
+ unique: [period: :infinity, states: [:available, :scheduled], keys: [:domain]]
+
+ alias Pleroma.HTTP
+ alias Pleroma.Instances
+
+ import Ecto.Query
+
+ @impl true
+ def perform(%Oban.Job{args: %{"domain" => domain, "phase" => phase, "attempt" => attempt}}) do
+ case check_reachability(domain) do
+ :ok ->
+ Instances.set_reachable("https://#{domain}")
+ :ok
+
+ {:error, _} = error ->
+ handle_failed_attempt(domain, phase, attempt)
+ error
+ end
+ end
+
+ # New jobs enter here and are immediately re-scheduled for the first phase
+ @impl true
+ def perform(%Oban.Job{args: %{"domain" => domain}}) do
+ scheduled_at = DateTime.add(DateTime.utc_now(), 60, :second)
+
+ %{
+ "domain" => domain,
+ "phase" => "phase_1min",
+ "attempt" => 1
+ }
+ |> new(scheduled_at: scheduled_at, replace: true)
+ |> Oban.insert()
+
+ :ok
+ end
+
+ @impl true
+ def timeout(_job), do: :timer.seconds(5)
+
+ @doc "Deletes scheduled jobs to check reachability for specified instance"
+ def delete_jobs_for_host(host) do
+ Oban.Job
+ |> where(worker: "Pleroma.Workers.ReachabilityWorker")
+ |> where([j], j.args["domain"] == ^host)
+ |> Oban.delete_all_jobs()
+ end
+
+ defp check_reachability(domain) do
+ case HTTP.get("https://#{domain}/") do
+ {:ok, %{status: status}} when status in 200..299 ->
+ :ok
+
+ {:ok, %{status: _status}} ->
+ {:error, :unreachable}
+
+ {:error, _} = error ->
+ error
+ end
+ end
+
+ defp handle_failed_attempt(_domain, "final", _attempt), do: :ok
+
+ defp handle_failed_attempt(domain, phase, attempt) do
+ {interval_minutes, max_attempts, next_phase} = get_phase_config(phase)
+
+ if attempt >= max_attempts do
+ # Move to next phase
+ schedule_next_phase(domain, next_phase)
+ else
+ # Retry same phase with incremented attempt
+ schedule_retry(domain, phase, attempt + 1, interval_minutes)
+ end
+ end
+
+ defp get_phase_config("phase_1min"), do: {1, 4, "phase_15min"}
+ defp get_phase_config("phase_15min"), do: {15, 4, "phase_1hour"}
+ defp get_phase_config("phase_1hour"), do: {60, 4, "phase_8hour"}
+ defp get_phase_config("phase_8hour"), do: {480, 4, "phase_24hour"}
+ defp get_phase_config("phase_24hour"), do: {1440, 4, "final"}
+ defp get_phase_config("final"), do: {nil, 0, nil}
+
+ defp schedule_next_phase(_domain, "final"), do: :ok
+
+ defp schedule_next_phase(domain, next_phase) do
+ {interval_minutes, _max_attempts, _next_phase} = get_phase_config(next_phase)
+ scheduled_at = DateTime.add(DateTime.utc_now(), interval_minutes * 60, :second)
+
+ %{
+ "domain" => domain,
+ "phase" => next_phase,
+ "attempt" => 1
+ }
+ |> new(scheduled_at: scheduled_at, replace: true)
+ |> Oban.insert()
+ end
+
+ def schedule_retry(domain, phase, attempt, interval_minutes) do
+ scheduled_at = DateTime.add(DateTime.utc_now(), interval_minutes * 60, :second)
+
+ %{
+ "domain" => domain,
+ "phase" => phase,
+ "attempt" => attempt
+ }
+ |> new(scheduled_at: scheduled_at, replace: true)
+ |> Oban.insert()
+ end
+end
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
index 11b672bef..e2c950967 100644
--- a/lib/pleroma/workers/receiver_worker.ex
+++ b/lib/pleroma/workers/receiver_worker.ex
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.ReceiverWorker do
+ alias Pleroma.Instances
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.Federator
@@ -37,6 +38,11 @@ defmodule Pleroma.Workers.ReceiverWorker do
{:ok, _public_key} <- Signature.refetch_public_key(conn_data),
{:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
{:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
+ unless Instances.reachable?(params["actor"]) do
+ domain = URI.parse(params["actor"]).host
+ Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
+ end
+
{:ok, res}
else
e -> process_errors(e)
@@ -45,6 +51,11 @@ defmodule Pleroma.Workers.ReceiverWorker do
def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
+ unless Instances.reachable?(params["actor"]) do
+ domain = URI.parse(params["actor"]).host
+ Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
+ end
+
{:ok, res}
else
e -> process_errors(e)
diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex
index aa09362f5..0cc480c02 100644
--- a/lib/pleroma/workers/remote_fetcher_worker.ex
+++ b/lib/pleroma/workers/remote_fetcher_worker.ex
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.RemoteFetcherWorker do
+ alias Pleroma.Instances
alias Pleroma.Object.Fetcher
use Oban.Worker, queue: :background, unique: [period: :infinity]
@@ -11,6 +12,11 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do
def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do
case Fetcher.fetch_object_from_id(id, depth: args["depth"]) do
{:ok, _object} ->
+ unless Instances.reachable?(id) do
+ # Mark the server as reachable since we successfully fetched an object
+ Instances.set_reachable(id)
+ end
+
:ok
{:allowed_depth, false} ->
diff --git a/mix.exs b/mix.exs
index 379694b55..608032c5a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -4,8 +4,8 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
- version: version("2.7.0"),
- elixir: "~> 1.13",
+ version: version("2.9.1"),
+ elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors(), prune_code_paths: false],
@@ -37,22 +37,13 @@ defmodule Pleroma.Mixfile do
pleroma: [
include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load, eldap: :transient],
- steps: [:assemble, &put_otp_version/1, ©_files/1, ©_nginx_config/1],
+ steps: [:assemble, ©_files/1, ©_nginx_config/1],
config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, []}]
]
]
]
end
- def put_otp_version(%{path: target_path} = release) do
- File.write!(
- Path.join([target_path, "OTP_VERSION"]),
- Pleroma.OTPVersion.version()
- )
-
- release
- end
-
def copy_files(%{path: target_path} = release) do
File.cp_r!("./rel/files", target_path)
release
@@ -132,22 +123,23 @@ defmodule Pleroma.Mixfile do
# Type `mix help deps` for examples and options.
defp deps do
[
- {:phoenix, "~> 1.7.3"},
+ {:phoenix,
+ git: "https://github.com/feld/phoenix", branch: "v1.7.14-websocket-headers", override: true},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.10"},
{:ecto_enum, "~> 1.4"},
- {:postgrex, ">= 0.0.0"},
+ {:postgrex, ">= 0.20.0"},
{:phoenix_html, "~> 3.3"},
{:phoenix_live_view, "~> 0.19.0"},
{:phoenix_live_dashboard, "~> 0.8.0"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:tzdata, "~> 1.0.3"},
- {:plug_cowboy, "~> 2.5"},
- {:oban, "~> 2.18.0"},
{:oban_plugins_lazarus,
git: "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git",
ref: "e49fc355baaf0e435208bf5f534d31e26e897711"},
+ {:plug_cowboy, "~> 2.7"},
+ {:oban, "~> 2.19.0"},
{:gettext, "~> 0.20"},
{:bcrypt_elixir, "~> 2.2"},
{:trailing_format_plug, "~> 0.0.7"},
@@ -156,9 +148,9 @@ defmodule Pleroma.Mixfile do
{:calendar, "~> 1.0"},
{:cachex, "~> 3.2"},
{:tesla, "~> 1.11"},
- {:castore, "~> 0.1"},
- {:cowlib, "~> 2.9", override: true},
- {:gun, "~> 2.0.0-rc.1", override: true},
+ {:castore, "~> 1.0"},
+ {:cowlib, "~> 2.15"},
+ {:gun, "~> 2.2"},
{:finch, "~> 0.15"},
{:jason, "~> 1.2"},
{:mogrify, "~> 0.9.0", override: "true"},
@@ -172,6 +164,8 @@ defmodule Pleroma.Mixfile do
{:swoosh, "~> 1.16.9"},
{:phoenix_swoosh, "~> 1.1"},
{:gen_smtp, "~> 0.13"},
+ {:mua, "~> 0.2.0"},
+ {:mail, "~> 0.3.0"},
{:ex_syslogger, "~> 1.4"},
{:floki, "~> 0.35"},
{:timex, "~> 3.6"},
@@ -193,11 +187,11 @@ defmodule Pleroma.Mixfile do
ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"},
{:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
- ref: "6630c42aaaab124e697b4e513190c89d8b64e410"},
+ ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"},
{:restarter, path: "./restarter"},
{:majic, "~> 1.0"},
{:open_api_spex, "~> 3.16"},
- {:ecto_psql_extras, "~> 0.6"},
+ {:ecto_psql_extras, "~> 0.8"},
{:vix, "~> 0.26.0"},
{:elixir_make, "~> 0.7.7", override: true},
{:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash},
@@ -213,7 +207,7 @@ defmodule Pleroma.Mixfile do
{:poison, "~> 3.0", only: :test},
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:ex_machina, "~> 2.4", only: :test},
- {:credo, "~> 1.6", only: [:dev, :test], runtime: false},
+ {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.5", only: :test},
{:covertool, "~> 2.0", only: :test},
{:hackney, "~> 1.18.0", override: true},
@@ -236,7 +230,7 @@ defmodule Pleroma.Mixfile do
"ecto.rollback": ["pleroma.ecto.rollback"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
- test: ["ecto.create --quiet", "ecto.migrate", "test"],
+ test: ["ecto.create --quiet", "ecto.migrate", "test --warnings-as-errors"],
docs: ["pleroma.docs", "docs"],
analyze: ["credo --strict --only=warnings,todo,fixme,consistency,readability"],
copyright: &add_copyright/1,
diff --git a/mix.lock b/mix.lock
index 740035698..3599f62b4 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,121 +1,124 @@
%{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
- "argon2_elixir": {:hex, :argon2_elixir, "4.0.0", "7f6cd2e4a93a37f61d58a367d82f830ad9527082ff3c820b8197a8a736648941", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f9da27cf060c9ea61b1bd47837a28d7e48a8f6fa13a745e252556c14f9132c7f"},
- "bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"},
+ "argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"},
+ "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
- "benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"},
+ "benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"},
"blurhash": {:hex, :rinpatch_blurhash, "0.1.0", "01a888b0f5f1f382ab52e4396f01831cbe8486ea5828604c90f4dac533d39a4b", [:mix], [{:mogrify, "~> 0.8.0", [hex: :mogrify, repo: "hexpm", optional: true]}], "hexpm", "19911a5dcbb0acb9710169a72f702bce6cb048822b12de566ccd82b2cc42b907"},
"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.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
- "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "6630c42aaaab124e697b4e513190c89d8b64e410", [ref: "6630c42aaaab124e697b4e513190c89d8b64e410"]},
- "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
- "cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
+ "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", "e7b7cc34cc16b383461b966484c297e4ec9aeef6", [ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"]},
+ "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"},
+ "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"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.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
"concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cors_plug": {:hex, :cors_plug, "2.0.3", "316f806d10316e6d10f09473f19052d20ba0a0ce2a1d910ddf57d663dac402ae", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ee4ae1418e6ce117fc42c2ba3e6cbdca4e95ecd2fe59a05ec6884ca16d469aea"},
- "covertool": {:hex, :covertool, "2.0.6", "4a291b4e3449025b0595d8f44c8d7635d4f48f033be2ce88d22a329f36f94a91", [:rebar3], [], "hexpm", "5db3fcd82180d8ea4ad857d4d1ab21a8d31b5aee0d60d2f6c0f9e25a411d1e21"},
- "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"},
+ "covertool": {:hex, :covertool, "2.0.7", "398be995c4cf1a2861174389b3577ca97beee43b60c8f1afcf510f1b07d32408", [:rebar3], [], "hexpm", "46158ed6e1a0df7c0a912e314c7b8e053bd74daa5fc6b790614922a155b5720c"},
+ "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
"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"},
+ "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
+ "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [: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", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
- "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
- "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
+ "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"},
+ "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"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.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"},
"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.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"eblurhash": {:git, "https://github.com/zotonic/eblurhash.git", "bc37ceb426ef021ee9927fb249bb93f7059194ab", [ref: "bc37ceb426ef021ee9927fb249bb93f7059194ab"]},
- "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 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", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
+ "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 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", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
- "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.15", "0fc29dbae0e444a29bd6abeee4cf3c4c037e692a272478a234a1cc765077dbb1", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "b6127f3a5c6fc3d84895e4768cc7c199f22b48b67d6c99b13fbf4a374e73f039"},
- "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 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", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"},
+ "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.8", "aa02529c97f69aed5722899f5dc6360128735a92dd169f23c5d50b1f7fdede08", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "04c63d92b141723ad6fed2e60a4b461ca00b3594d16df47bbc48f1f4534f2c49"},
+ "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"},
"eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"},
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
- "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
+ "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"},
- "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"},
- "ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"},
- "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
- "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_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [: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", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"},
+ "ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"},
+ "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [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", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
+ "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, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
"exile": {:hex, :exile, "0.10.0", "b69e2d27a9af670b0f0a0898addca0eda78f6f5ba95ccfbc9bc6ccdd04925436", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "c62ee8fee565b5ac4a898d0dcd58d2b04fb5eec1655af1ddcc9eb582c6732c33"},
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
- "fast_html": {:hex, :fast_html, "2.2.0", "6c5ef1be087a4ed613b0379c13f815c4d11742b36b67bb52cee7859847c84520", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "064c4f23b4a6168f9187dac8984b056f2c531bb0787f559fd6a8b34b38aefbae"},
+ "fast_html": {:hex, :fast_html, "2.5.0", "23578c1c1fa03db6a3786d78218b2d944df34b0fc3729e72de912f390913c80f", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "69eb46ed98a5d9cca1ccd4a5ac94ce5dd626fc29513fbaa0a16cd8b2da67ae3e"},
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
- "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
+ "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
- "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
+ "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
- "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"},
- "hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [: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", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"},
- "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"},
+ "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"},
+ "hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [: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", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"},
+ "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
- "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
+ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
"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.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"},
- "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
+ "joken": {:hex, :joken, "2.6.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"},
"logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"},
- "majic": {:hex, :majic, "1.0.0", "37e50648db5f5c2ff0c9fb46454d034d11596c03683807b9fb3850676ffdaab3", [:make, :mix], [{:elixir_make, "~> 0.6.1", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7905858f76650d49695f14ea55cd9aaaee0c6654fa391671d4cf305c275a0a9e"},
+ "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
+ "majic": {:hex, :majic, "1.1.1", "16092a3a3376cc5e13d207e82ec06e05a5561170465e50cc11cc4df8a5747938", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7fbb0372f0447b3f777056177d6ab3f009742e68474f850521ff56b84bd85b96"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
- "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"},
+ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
- "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
- "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"},
+ "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
+ "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
- "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"},
+ "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"},
"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"},
+ "mua": {:hex, :mua, "0.2.4", "a9172ab0a1ac8732cf2699d739ceac3febcb9b4ffc540260ad2e32c0b6632af9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "e7e4dacd5ad65f13e3542772e74a159c00bd2d5579e729e9bb72d2c73a266fb7"},
"multipart": {:hex, :multipart, "0.4.0", "634880a2148d4555d050963373d0e3bbb44a55b2badd87fa8623166172e9cda0", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "3c5604bc2fb17b3137e5d2abdf5dacc2647e60c5cc6634b102cf1aef75a06f0a"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
+ "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
- "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
+ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
- "oban": {:hex, :oban, "2.18.2", "583e78965ee15263ac968e38c983bad169ae55eadaa8e1e39912562badff93ba", [: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", "9dd25fd35883a91ed995e9fe516e479344d3a8623dfe2b8c3fc8e5be0228ec3a"},
+ "oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"},
"oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"},
"oban_plugins_lazarus": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", "e49fc355baaf0e435208bf5f534d31e26e897711", [ref: "e49fc355baaf0e435208bf5f534d31e26e897711"]},
"octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
- "open_api_spex": {:hex, :open_api_spex, "3.18.2", "8c855e83bfe8bf81603d919d6e892541eafece3720f34d1700b58024dadde247", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "aa3e6dcfc0ad6a02596b2172662da21c9dd848dac145ea9e603f54e3d81b8d2b"},
+ "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [: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", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
- "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.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
+ "phoenix": {:git, "https://github.com/feld/phoenix", "fb6dc76c657422e49600896c64aab4253fceaef6", [branch: "v1.7.14-websocket-headers"]},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [: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", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"},
"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.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [: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]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [: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]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [: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", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
- "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
- "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"},
- "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
+ "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [: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", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [: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", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"},
+ "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"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, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
- "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
+ "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [: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", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"},
"pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
- "prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5 or ~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"},
+ "prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"},
"prometheus": {:hex, :prometheus, "4.10.0", "792adbf0130ff61b5fa8826f013772af24b6e57b984445c8d602c8a0355704a1", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "2a99bb6dce85e238c7236fde6b0064f9834dc420ddbd962aac4ea2a3c3d59384"},
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"},
"prometheus_ex": {:git, "https://github.com/lanodan/prometheus.ex.git", "31f7fbe4b71b79ba27efc2a5085746c4011ceb8f", [branch: "fix/elixir-1.14"]},
@@ -123,33 +126,33 @@
"prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]},
"prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"},
"quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"},
- "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
- "recon": {:hex, :recon, "2.5.4", "05dd52a119ee4059fa9daa1ab7ce81bc7a8161a2f12e9d42e9d551ffd2ba901c", [:mix, :rebar3], [], "hexpm", "e9ab01ac7fc8572e41eb59385efeb3fb0ff5bf02103816535bacaedf327d0263"},
+ "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
+ "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
"rustler": {:hex, :rustler, "0.30.0", "cefc49922132b072853fa9b0ca4dc2ffcb452f68fb73b779042b02d545e097fb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "9ef1abb6a7dda35c47cfc649e6a5a61663af6cf842a55814a554a84607dee389"},
- "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"},
+ "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
- "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
- "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
- "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {: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]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"},
+ "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"},
+ "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
+ "swoosh": {:hex, :swoosh, "1.16.12", "cbb24ad512f2f7f24c7a469661c188a00a8c2cd64e0ab54acd1520f132092dfd", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {: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]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e262df1ae510d59eeaaa3db42189a2aa1b3746f73771eb2616fc3f7ee63cc20"},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
- "table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
+ "table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"},
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
"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_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.0", "b583c3f18508f5c5561b674d16cf5d9afd2ea3c04505b7d92baaeac93c1b8260", [: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", "9cba950e1c4733468efbe3f821841f34ac05d28e7af7798622f88ecdbbe63ea3"},
- "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
- "tesla": {:hex, :tesla, "1.11.0", "81b2b10213dddb27105ec6102d9eb0cc93d7097a918a0b1594f2dfd1a4601190", [: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", "b83ab5d4c2d202e1ea2b7e17a49f788d49a699513d7c4f08f2aef2c281be69db"},
- "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
+ "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
+ "tesla": {:hex, :tesla, "1.15.3", "3a2b5c37f09629b8dcf5d028fbafc9143c0099753559d7fe567eaabfbd9b8663", [: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.21", [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", "98bb3d4558abc67b92fb7be4cd31bb57ca8d80792de26870d362974b58caeda7"},
+ "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"},
"timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"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.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"},
- "ueberauth": {:hex, :ueberauth, "0.10.7", "5a31cbe11e7ce5c7484d745dc9e1f11948e89662f8510d03c616de03df581ebd", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0bccf73e2ffd6337971340832947ba232877aa8122dba4c95be9f729c8987377"},
- "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
+ "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"},
+ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
- "vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 1.0 or ~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.2 or ~> 0.1.4", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8 or ~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"},
+ "vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"},
"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.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [: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", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
}
diff --git a/priv/gettext/fr/LC_MESSAGES/config_descriptions.po b/priv/gettext/fr/LC_MESSAGES/config_descriptions.po
index e43db68aa..c24ab6751 100644
--- a/priv/gettext/fr/LC_MESSAGES/config_descriptions.po
+++ b/priv/gettext/fr/LC_MESSAGES/config_descriptions.po
@@ -3,14 +3,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-22 02:09+0300\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: Automatically generated\n"
-"Language-Team: none\n"
+"PO-Revision-Date: 2024-10-13 21:03+0000\n"
+"Last-Translator: Codimp \n"
+"Language-Team: French \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Translate Toolkit 3.7.2\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 4.13.1\n"
## This file is a PO Template file.
##
@@ -21,7 +23,6 @@ msgstr ""
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here has no
## effect: edit them in PO (.po) files instead.
-
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :esshd"
@@ -32,25 +33,30 @@ msgstr ""
#, elixir-autogen, elixir-format
msgctxt "config description at :logger"
msgid "Logger-related settings"
-msgstr ""
+msgstr "Paramètres liés à la journalisation"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :mime"
msgid "Mime Types settings"
-msgstr ""
+msgstr "Paramètres des types Mime"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma"
msgid "Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)"
msgstr ""
+"Permet de configurer un jeton qui peut être utilisé pour authentifier les "
+"requêtes avec des privilèges administrateurs sans utiliser un jeton de "
+"compte utilisateur standard. Pour l'utiliser, ajoutez le paramètre "
+"`admin_token`aux requêtes. (Vous devriez utiliser l'authentification HTTP "
+"Basic ou OAuth à la place si vous le pouvez)"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma"
msgid "Authenticator"
-msgstr ""
+msgstr "Authentifieur"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
@@ -62,7 +68,7 @@ msgstr ""
#, elixir-autogen, elixir-format
msgctxt "config label at :cors_plug"
msgid "CORS plug config"
-msgstr ""
+msgstr "Configuration du plug CORS"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
@@ -74,25 +80,25 @@ msgstr ""
#, elixir-autogen, elixir-format
msgctxt "config label at :logger"
msgid "Logger"
-msgstr ""
+msgstr "Journaliseur"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :mime"
msgid "Mime Types"
-msgstr ""
+msgstr "Types Mime"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma"
msgid "Pleroma Admin Token"
-msgstr ""
+msgstr "Jeton Administrateur Pleroma"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma"
msgid "Pleroma Authenticator"
-msgstr ""
+msgstr "Authentifieur Pleroma"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
@@ -104,103 +110,111 @@ msgstr ""
#, elixir-autogen, elixir-format
msgctxt "config description at :logger-:console"
msgid "Console logger settings"
-msgstr ""
+msgstr "Paramètres de journalisation de la console"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger-:ex_syslogger"
msgid "ExSyslogger-related settings"
-msgstr ""
+msgstr "Paramètres liés à ExSyslogger"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:activitypub"
msgid "ActivityPub-related settings"
-msgstr ""
+msgstr "Paramètres liés à ActivityPub"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:assets"
msgid "This section configures assets to be used with various frontends. Currently the only option relates to mascots on the mastodon frontend"
msgstr ""
+"Cette section configure les annexes (assets) à utiliser avec divers "
+"frontaux. La seule option est actuellement liée au mascottes du frontal "
+"mastodon"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:auth"
msgid "Authentication / authorization settings"
-msgstr ""
+msgstr "Paramètres d'authentification/autorisations"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:connections_pool"
msgid "Advanced settings for `Gun` connections pool"
-msgstr ""
+msgstr "Paramètres avancés pour le bac (pool) de connexions `Gun`"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:email_notifications"
msgid "Email notifications settings"
-msgstr ""
+msgstr "Paramètres de notification par email"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:features"
msgid "Customizable features"
-msgstr ""
+msgstr "Fonctionnalités personnalisables"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:feed"
msgid "Configure feed rendering"
-msgstr ""
+msgstr "Configurer le rendu des flux"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations"
msgid "This form can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for pleroma_fe are configured. If you want to add your own configuration your settings all fields must be complete."
msgstr ""
+"Ce formulaire peut être utilisé pour configurer une liste de clés (keyword) "
+"qui contiennent les données de configuration pour tout types de frontaux. "
+"Par défaut, les paramètres pour pleroma_fe sont configurés. Si vous voulez "
+"ajouter vos propres paramètres de configurations, tout les champs doivent "
+"être remplis."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends"
msgid "Installed frontends management"
-msgstr ""
+msgstr "Gestion des frontaux installés"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:gopher"
msgid "Gopher settings"
-msgstr ""
+msgstr "Paramètres Gopher"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools"
msgid "Advanced settings for `Hackney` connections pools"
-msgstr ""
+msgstr "Paramètres avancés pour les bacs (pool) de connexions `Hackney`"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http"
msgid "HTTP settings"
-msgstr ""
+msgstr "Paramètres HTTP"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http_security"
msgid "HTTP security settings"
-msgstr ""
+msgstr "Paramètres de sécurité HTTP"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance"
msgid "Instance-related settings"
-msgstr ""
+msgstr "Paramètres liés à l'instance"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instances_favicons"
msgid "Control favicons for instances"
-msgstr ""
+msgstr "Gère les favicons des instances"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
@@ -212,151 +226,177 @@ msgstr ""
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:majic_pool"
msgid "Majic/libmagic configuration"
-msgstr ""
+msgstr "Configuration de majic/libmagic"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:manifest"
msgid "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE."
msgstr ""
+"Cette section décrit les valeurs spécifique à l'instance du manifeste PWA. "
+"Actuellement, cette option ne concerne que MastoFE."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_preview_proxy"
msgid "Media preview proxy"
-msgstr ""
+msgstr "Proxy de prévisualisation média"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy"
msgid "Media proxy"
-msgstr ""
+msgstr "Proxy média"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:modules"
msgid "Custom Runtime Modules"
-msgstr ""
+msgstr "Modules Runtime Personalisés"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf"
msgid "General MRF settings"
-msgstr ""
+msgstr "Paramètres généraux MRF"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_activity_expiration"
msgid "Adds automatic expiration to all local activities"
-msgstr ""
+msgstr "Ajoute une expiration automatique à toutes les activités locales"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_follow_bot"
msgid "Automatically follows newly discovered accounts."
-msgstr ""
+msgstr "Suivre automatiquement les comptes venant d'être découverts."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_hashtag"
msgid "Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (without the leading #)\n\nNote: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.\n"
msgstr ""
+"Rejeter, Enlever de TWKN ou marquer comme contenu sensible les messages avec "
+"des mots-croisillons (sans mettre le # du début)\n"
+"\n"
+"Note: cette politique MRF est toujours activée. Si vous voulez la "
+"désactiver, vous devez configurer des listes vides.\n"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_hellthread"
msgid "Block messages with excessive user mentions"
-msgstr ""
+msgstr "Bloquer les messages avec un nombre excessif de mentions"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_keyword"
msgid "Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html)."
msgstr ""
+"Rejeter ou remplacer les mots des messages qui correspondent à un mot clef "
+"ou à une [expression rationnelle (Regex)](https://hexdocs.pm/elixir/Regex."
+"html)."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_mention"
msgid "Block messages which mention a specific user"
-msgstr ""
+msgstr "Bloquer les messages mentionnant un utilisateur particulier"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_normalize_markup"
msgid "MRF NormalizeMarkup settings. Scrub configured hypertext markup."
msgstr ""
+"Paramètres de normalisation MRF. Balaie les balises hypertextes configurées."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_object_age"
msgid "Rejects or delists posts based on their timestamp deviance from your server's clock."
msgstr ""
+"Rejette ou retire des listes les messages selon l'écart entre leur heure et "
+"l'horloge de votre serveur."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_rejectnonpublic"
msgid "RejectNonPublic drops posts with non-public visibility settings."
msgstr ""
+"RejectNonPublic enlève les messages avec des paramètres de visibilité non-"
+"publics."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple"
msgid "Simple ingress policies"
-msgstr ""
+msgstr "Politiques simples pour entrants"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_steal_emoji"
msgid "Steals emojis from selected instances when it sees them."
-msgstr ""
+msgstr "Vole les emojis des instances sélectionnées quand il les voit."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_subchain"
msgid "This policy processes messages through an alternate pipeline when a given message matches certain criteria. All criteria are configured as a map of regular expressions to lists of policy modules."
msgstr ""
+"Cette politique traite les messages à travers un tuyau séparé lorsqu'un "
+"message donné correspond à certain critères. Chaque critère est configuré "
+"comme une correspondance entre une expression rationnelle et une liste de "
+"modules de politiques."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_vocabulary"
msgid "Filter messages which belong to certain activity vocabularies"
msgstr ""
+"Filtrer les messages qui correspondent à certain vocabulaires d'activités"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:oauth2"
msgid "Configure OAuth 2 provider capabilities"
-msgstr ""
+msgstr "Configurer les capacités du fournisseur OAuth 2"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools"
msgid "Advanced settings for `Gun` workers pools"
-msgstr ""
+msgstr "Paramètres avancés pour les bacs (pools) de travailleurs `Gun`"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:populate_hashtags_table"
msgid "`populate_hashtags_table` background migration settings"
-msgstr ""
+msgstr "Paramètres de migration en arrière-plan `populate_hashtags_table`"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit"
msgid "Rate limit settings. This is an advanced feature enabled only for :authentication by default."
msgstr ""
+"Paramètres de limites par secondes. C'est une fonctionnalité avancée qui, "
+"par défaut, n'est activée que pour :authentication."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated"
msgid "Disallow viewing timelines, user profiles and statuses for unauthenticated users."
msgstr ""
+"Empêche de regarder les flux, les profils utilisateurs et les status pour "
+"les utilisateurs non-authentifiés."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rich_media"
msgid "If enabled the instance will parse metadata from attached links to generate link previews"
msgstr ""
+"Si activé, l'instance interprétera les métadonnées des liens joins pour "
+"générer les prévisualisations de liens"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
@@ -369,6 +409,8 @@ msgstr ""
msgctxt "config description at :pleroma-:static_fe"
msgid "Render profiles and posts using server-generated HTML that is viewable without using JavaScript"
msgstr ""
+"Rendre les profils et les status en utilisant du HTML généré par le serveur "
+"qui ne nécessitera pas de JavaScript"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
@@ -380,7 +422,7 @@ msgstr ""
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:uri_schemes"
msgid "URI schemes related settings"
-msgstr ""
+msgstr "Paramètres liés au schémas d'URI"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
diff --git a/priv/repo/migrations/20221203232118_add_user_follows_hashtag.exs b/priv/repo/migrations/20221203232118_add_user_follows_hashtag.exs
new file mode 100644
index 000000000..2b5ae91be
--- /dev/null
+++ b/priv/repo/migrations/20221203232118_add_user_follows_hashtag.exs
@@ -0,0 +1,14 @@
+defmodule Pleroma.Repo.Migrations.AddUserFollowsHashtag do
+ use Ecto.Migration
+
+ def change do
+ create table(:user_follows_hashtag) do
+ add(:hashtag_id, references(:hashtags))
+ add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
+ end
+
+ create(unique_index(:user_follows_hashtag, [:user_id, :hashtag_id]))
+
+ create_if_not_exists(index(:user_follows_hashtag, [:hashtag_id]))
+ end
+end
diff --git a/priv/repo/migrations/20240904142434_assign_app_user.exs b/priv/repo/migrations/20240904142434_assign_app_user.exs
index 11bec529b..74740220d 100644
--- a/priv/repo/migrations/20240904142434_assign_app_user.exs
+++ b/priv/repo/migrations/20240904142434_assign_app_user.exs
@@ -1,20 +1,24 @@
defmodule Pleroma.Repo.Migrations.AssignAppUser do
use Ecto.Migration
+ import Ecto.Query
+
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
def up do
- Repo.all(Token)
- |> Enum.group_by(fn x -> Map.get(x, :app_id) end)
- |> Enum.each(fn {_app_id, tokens} ->
- token =
- Enum.filter(tokens, fn x -> not is_nil(x.user_id) end)
- |> List.first()
-
+ Token
+ |> where([t], not is_nil(t.user_id))
+ |> group_by([t], t.app_id)
+ |> select([t], %{app_id: t.app_id, id: min(t.id)})
+ |> order_by(asc: :app_id)
+ |> Repo.stream()
+ |> Stream.each(fn %{id: id} ->
+ token = Token.Query.get_by_id(id) |> Repo.one()
App.maybe_update_owner(token)
end)
+ |> Stream.run()
end
def down, do: :ok
diff --git a/priv/repo/migrations/20250314153704_add_activities_actor_type_index.exs b/priv/repo/migrations/20250314153704_add_activities_actor_type_index.exs
new file mode 100644
index 000000000..a0fac28a8
--- /dev/null
+++ b/priv/repo/migrations/20250314153704_add_activities_actor_type_index.exs
@@ -0,0 +1,14 @@
+defmodule Pleroma.Repo.Migrations.AddActivitiesActorTypeIndex do
+ use Ecto.Migration
+ @disable_ddl_transaction true
+
+ def change do
+ create(
+ index(
+ :activities,
+ ["actor", "(data ->> 'type'::text)", "id DESC NULLS LAST"],
+ concurrently: true
+ )
+ )
+ end
+end
diff --git a/rel/vm.args.eex b/rel/vm.args.eex
index 71e803264..8e38fee4b 100644
--- a/rel/vm.args.eex
+++ b/rel/vm.args.eex
@@ -9,3 +9,8 @@
## Tweak GC to run more often
##-env ERL_FULLSWEEP_AFTER 10
+
+# Disable wasteful busywait.
++sbwt none
++sbwtdcpu none
++sbwtdio none
diff --git a/test/fixtures/break_analyze.png b/test/fixtures/break_analyze.png
new file mode 100644
index 000000000..b5e91b08a
Binary files /dev/null and b/test/fixtures/break_analyze.png differ
diff --git a/test/fixtures/friendica-dislike-undo.json b/test/fixtures/friendica-dislike-undo.json
new file mode 100644
index 000000000..b258e00be
--- /dev/null
+++ b/test/fixtures/friendica-dislike-undo.json
@@ -0,0 +1,76 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "Hashtag": "as:Hashtag",
+ "PropertyValue": "schema:PropertyValue",
+ "conversation": "ostatus:conversation",
+ "dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
+ "diaspora": "https://diasporafoundation.org/ns/",
+ "directMessage": "litepub:directMessage",
+ "discoverable": "toot:discoverable",
+ "featured": {
+ "@id": "toot:featured",
+ "@type": "@id"
+ },
+ "litepub": "http://litepub.social/ns#",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "ostatus": "http://ostatus.org#",
+ "quoteUrl": "as:quoteUrl",
+ "schema": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "value": "schema:value",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "actor": "https://my-place.social/profile/vaartis",
+ "cc": [
+ "https://my-place.social/followers/vaartis"
+ ],
+ "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182/Undo",
+ "instrument": {
+ "id": "https://my-place.social/friendica",
+ "name": "Friendica 'Interrupted Fern' 2024.12-1576",
+ "type": "Application",
+ "url": "https://my-place.social"
+ },
+ "object": {
+ "actor": "https://my-place.social/profile/vaartis",
+ "cc": [
+ "https://my-place.social/followers/vaartis"
+ ],
+ "diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302",
+ "diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}",
+ "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182",
+ "instrument": {
+ "id": "https://my-place.social/friendica",
+ "name": "Friendica 'Interrupted Fern' 2024.12-1576",
+ "type": "Application",
+ "url": "https://my-place.social"
+ },
+ "object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213",
+ "published": "2025-06-12T18:47:41Z",
+ "to": [
+ "https://pl.kotobank.ch/users/vaartis",
+ "https://mitra.social/users/silverpill",
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type": "Dislike"
+ },
+ "published": "2025-06-12T18:41:25Z",
+ "signature": {
+ "created": "2025-06-12T18:44:16Z",
+ "creator": "https://my-place.social/profile/vaartis#main-key",
+ "nonce": "2d67847d4bd4b1b83a30d61eac6cdc7ad6b980df06a8b9b97217e1d8f7b6cf20",
+ "signatureValue": "LnoRMZuQGDvTICkShGBq28ynaj2lF1bViJFGS6n4gKn3IbxPWATHxao43gxWRc+HCTrHNg7quzgaW4+PYM7UVUz3jO+bjNKsN845nijOVdyFrPOXbuaij3KQh2OoHhFJWoV/ZQQTFF0kRK1qT4BwG+P8NqOOKAMv+Cw7ruQH+f2w7uDgcNIbCD1gLcwb6cw7WVe5qu8yMkKqp2kBdqW3RCsI85RmmFgwehDgH5nrX7ER1qbeLWrqy7echwD9/fO3rqAu13xDNyiGZHDT7JB3RUt0AyMm0XCfjbwSQ0n+MkYXgE4asvFz81+iiPCLt+6gePWAFc5odF1FxdySBpSuUOs4p92NzP9OhQ0c0qrqrzYI7aYklY7oMfxjkva+X+0bm3up+2IRJdnZa/pXlmwdcqTpyMr1sgzaexMUNBp3dq7zA51eEaakLDX3i2onXJowfmze3+6XgPAFHYamR+pRNtuEoY4uyYEK3fj5GgwJ4RtFJMYVoEs/Q8h3OgYRcK1FE9UlDjSqbQ7QIRn2Ib4wjgmkeM0vrHIwh/1CtqA/M/6WuYFzCaJBc8O9ykpK9ZMbw64ToQXKf2SqhZsDoyTWRWTO1PXOk1XCAAElUh8/WCyeghvgqLXn0LHov4lmBsHA5iMUcLqBKD3GJIHd+ExrOFxMZs4mBLLGyz0p5joJ3NY=",
+ "type": "RsaSignature2017"
+ },
+ "to": [
+ "https://pl.kotobank.ch/users/vaartis",
+ "https://mitra.social/users/silverpill",
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type": "Undo"
+}
diff --git a/test/fixtures/friendica-dislike.json b/test/fixtures/friendica-dislike.json
new file mode 100644
index 000000000..c75939073
--- /dev/null
+++ b/test/fixtures/friendica-dislike.json
@@ -0,0 +1,56 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "Hashtag": "as:Hashtag",
+ "PropertyValue": "schema:PropertyValue",
+ "conversation": "ostatus:conversation",
+ "dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
+ "diaspora": "https://diasporafoundation.org/ns/",
+ "directMessage": "litepub:directMessage",
+ "discoverable": "toot:discoverable",
+ "featured": {
+ "@id": "toot:featured",
+ "@type": "@id"
+ },
+ "litepub": "http://litepub.social/ns#",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "ostatus": "http://ostatus.org#",
+ "quoteUrl": "as:quoteUrl",
+ "schema": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "value": "schema:value",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "actor": "https://my-place.social/profile/vaartis",
+ "cc": [
+ "https://my-place.social/followers/vaartis"
+ ],
+ "diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302",
+ "diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}",
+ "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182",
+ "instrument": {
+ "id": "https://my-place.social/friendica",
+ "name": "Friendica 'Interrupted Fern' 2024.12-1576",
+ "type": "Application",
+ "url": "https://my-place.social"
+ },
+ "object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213",
+ "published": "2025-06-12T18:47:41Z",
+ "signature": {
+ "created": "2025-06-12T18:47:42Z",
+ "creator": "https://my-place.social/profile/vaartis#main-key",
+ "nonce": "84e496f80b09d7a299c5cc89e8cadd13abf621b3a0a321684fa74278b68a6dd8",
+ "signatureValue": "qe2WxY+j7daIYLRadCctgal6A1s9XgoiMfM/8KjJm15w0sSizYYqruyQ5gS44e+cj5GHc9v5gP2ieod5v7eHAPzlcDI4bfkcyHVapAXTqU67ZebW+v6Q+21IMDgqrkYCv5TbV7LTxltW59dlqovpHE4TEe/M7xLKWJ3vVchRUcWqH9kDmak0nacoqYVAb5E9jYnQhUWPTCfPV82qQpeWQPOZ4iIvPw6rDkSSY5jL6bCogBZblHGpUjXfe/FPlacaCWiTQdoga3yOBXB1RYPw9nh5FI5Xkv/oi+52WmJrECinlD6AL8/BpiYvKz236zy7p/TR4BXlCx9RR/msjOnSabkQ4kmYFrRr80UDCGF+CdkdzLl8K9rSE3PbF1+nEqD7X0GOWn/DdtixqXJw6IR4bh32YW2SlcrSRBvI1p82Mv68BeqRaYqL6FAhKFwLhX5JpXngZ3k0g7rWWxc498voPWnFZDyCTRNxO9VIIUavDDEQ0BdFk6WDb8zx9tsAg8JoK57eVDcFly7tfVQffYiHpve06d8ag1DtzipqguRsURmuqpGNMq28XBTxwtrP2LnXXHKxoYN/YQ9cDnCKclbx7/uKmOVMLkLZlM0wAVoZpm5z2fG4voKqFiGZ1PoiFY2sN4URMArJtygV3PlTX4ASAQrak0ksvEo9msrBUD0Su9c=",
+ "type": "RsaSignature2017"
+ },
+ "to": [
+ "https://pl.kotobank.ch/users/vaartis",
+ "https://mitra.social/users/silverpill",
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type": "Dislike"
+}
diff --git a/test/fixtures/fulmo.html b/test/fixtures/fulmo.html
new file mode 100644
index 000000000..e54eaf8d8
--- /dev/null
+++ b/test/fixtures/fulmo.html
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Fulmo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ceteraj rakontoj
+ Bildosignoj
+ Eseoj
+ Ludoj
+ Poemoj
+ Vortaroj
+
+
+
+
+ Fulmo
+ Skribis Tirifto
+ 2025-01-31
+
+ »Kial ĉiam mi? Tio ne justas! Oni kulpigas min, sed ja ne mi kulpas!« La nubofeo lamentis, dum ĝi ordigis restaĵojn de falinta arbo. Plejparto el la pingloj estis brulintaj, kaj el la trunko ankoraŭ leviĝis fumo.
+ Subite aŭdeblis ekstraj kraketoj deapude. Ĝi rigardis flanken, kaj vidis iun kaŭri apud la arbo, derompi branĉetojn, kaj orde ilin amasigi. Ŝajnis, ke ekde sia rimarkiĝo, la nekonatulo laŭeble kuntiriĝis, kaj strebis labori kiel eble plej silente.
+ »Saluton…?« La nubofeo stariĝis, alporolante la eston. Tiu kvazaŭ frostiĝis, sed timeme ankaŭ stariĝis.
+ »S- Saluton…« Ĝi respondis sen kuraĝo rigardi ĝiadirekten. Nun stare, videblis ke ĝi estas verdanta florofeo.
+ »… kion vi faras tie ĉi?« La nubofeo demandis.
+ »Nu… tiel kaj tiel… mi ordigas.«
+ »Ho. Mi ricevis taskon ordigi ĉi tie… se vi povas atendi, vi ne bezonas peni!«
+ »N- Nu… mi tamen volus…« Parolis la florofeo, plu deturnante la kapon.
+ »Nu… bone, se vi tion deziras… dankon!« La nubofeo dankis, kaj returniĝis al sia laboro.
+ Fojfoje ĝi scivole rigardis al sia nova kunlaboranto, kaj fojfoje renkontis similan rigardon de ĝia flanko, kiuokaze ambaŭ rigardoj rapide revenis al la ordigataj pingloj kaj branĉetoj. »(Kial tiom volonte helpi min?)« Pensis al si la nubofeo. »(Ĉu ĝi simple tiom bonkoras? Ĝi ja tre bele floras; eble ankaŭ ĝia koro tiel same belas…)« Kaj vere, ĝiaj surfloroj grandanime malfermis siajn belkolorajn folietojn, kaj bonodoris al mondo.
+
+
+
+
+
+
+ Pinglordigado
+
+ © 2025 Tirifto
+
+
+
+
+ Post iom da tempo, ĉiu feo tralaboris ĝis la trunkomezo, kaj proksimiĝis al la alia feo. Kaj tiam ekpezis sur ili devosento rompi la silenton.
+ »… kia bela vetero, ĉu ne?« Diris la nubofeo, tuj rimarkonte, ke mallumiĝas, kaj la ĉielo restas kovrita de nuboj.
+ »Jes ja! Tre nube. Mi ŝatas nubojn!« Respondis la alia entuziasme, sed tuj haltetis kaj deturnis la kapon. Ambaŭ feoj daŭrigis laboron silente, kaj plu proksimiĝis, ĝis tiu preskaŭ estis finita.
+ »H… H… Ho ne…!« Subite ekdiris la nubofeo urĝe.
+ »Kio okazas?!«
+ »T… Tern…!«
+ »Jen! Tenu!« La florofeo etendis manon kun granda folio. La nubofeo ĝin prenis, kaj tien ternis. Aperis ekfulmo, kaj la cindriĝinta folio disfalis.
+ »Pardonu… mi ne volis…« Bedaŭris la nubofeo. »Mi ne scias, kial tio ĉiam okazas! Tiom plaĉas al mi promeni tere, sed ĉiuj diras, ke mi maldevus, ĉar ial ĝi ĉiam finiĝas tiel ĉi.« Ĝi montris al la arbo. »Eble ili pravas…«
+ »Nu…« diris la florofeo bedaŭre, kaj etendis la manon.
+ »H… H… Ne ree…!«
+ Ekfulmis. Alia ĵus metita folio cindriĝis en la manoj de la florofeo, time ferminta la okulojn.
+ »Dankegon… mi tre ŝatas vian helpon! Kaj mi ne… ne…«
+ Metiĝis. Ekfulmis. Cindriĝis.
+ »Io tre iritas mian nazon!« Plendis la nubofeo. Poste ĝi rimarkis la florpolvon, kiu disŝutiĝis el la florofeo en la tutan ĉirkaŭaĵon, kaj eĉ tuj antaŭ la nubofeon.
+ »N- Nu…« Diris la florofeo, honte rigardanta la teron. »… pardonu.«
+
+ Ĉu vi ŝatas la verkon? Subtenu min aŭ kopiu adreson de la verko por diskonigi ĝin!
+
+
+ Historio
+
+ 2025-01-31
+ Unua publikigo.
+
+
+
+ Permesilo
+ Ĉi tiun verkon vi rajtas libere kopii, disdoni, kaj ŝanĝi, laŭ kondiĉoj de la Permesilo de arto libera . (Resume: Vi devas mencii la aŭtoron kaj doni ligilon al la verko. Se vi ŝanĝas la verkon, vi devas laŭeble noti la faritajn ŝanĝojn, ilian daton, kaj eldoni ilin sub la sama permesilo.)
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/mastodon-update-with-likes.json b/test/fixtures/mastodon-update-with-likes.json
new file mode 100644
index 000000000..3bdb3ba3d
--- /dev/null
+++ b/test/fixtures/mastodon-update-with-likes.json
@@ -0,0 +1,90 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "atomUri": "ostatus:atomUri",
+ "conversation": "ostatus:conversation",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "ostatus": "http://ostatus.org#",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "votersCount": "toot:votersCount"
+ },
+ "https://w3id.org/security/v1"
+ ],
+ "actor": "https://pol.social/users/mkljczk",
+ "cc": ["https://www.w3.org/ns/activitystreams#Public",
+ "https://pol.social/users/aemstuz", "https://gts.mkljczk.pl/users/mkljczk",
+ "https://pl.fediverse.pl/users/mkljczk",
+ "https://fedi.kutno.pl/users/mkljczk"],
+ "id": "https://pol.social/users/mkljczk/statuses/113907871635572263#updates/1738096776",
+ "object": {
+ "atomUri": "https://pol.social/users/mkljczk/statuses/113907871635572263",
+ "attachment": [],
+ "attributedTo": "https://pol.social/users/mkljczk",
+ "cc": ["https://www.w3.org/ns/activitystreams#Public",
+ "https://pol.social/users/aemstuz", "https://gts.mkljczk.pl/users/mkljczk",
+ "https://pl.fediverse.pl/users/mkljczk",
+ "https://fedi.kutno.pl/users/mkljczk"],
+ "content": "test
",
+ "contentMap": {
+ "pl": "test
"
+ },
+ "conversation": "https://fedi.kutno.pl/contexts/43c14c70-d3fb-42b4-a36d-4eacfab9695a",
+ "id": "https://pol.social/users/mkljczk/statuses/113907871635572263",
+ "inReplyTo": "https://pol.social/users/aemstuz/statuses/113907854282654767",
+ "inReplyToAtomUri": "https://pol.social/users/aemstuz/statuses/113907854282654767",
+ "likes": {
+ "id": "https://pol.social/users/mkljczk/statuses/113907871635572263/likes",
+ "totalItems": 1,
+ "type": "Collection"
+ },
+ "published": "2025-01-28T20:29:45Z",
+ "replies": {
+ "first": {
+ "items": [],
+ "next": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies?only_other_accounts=true&page=true",
+ "partOf": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies",
+ "type": "CollectionPage"
+ },
+ "id": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies",
+ "type": "Collection"
+ },
+ "sensitive": false,
+ "shares": {
+ "id": "https://pol.social/users/mkljczk/statuses/113907871635572263/shares",
+ "totalItems": 0,
+ "type": "Collection"
+ },
+ "summary": null,
+ "tag": [
+ {
+ "href": "https://pol.social/users/aemstuz",
+ "name": "@aemstuz",
+ "type": "Mention"
+ },
+ {
+ "href": "https://gts.mkljczk.pl/users/mkljczk",
+ "name": "@mkljczk@gts.mkljczk.pl",
+ "type": "Mention"
+ },
+ {
+ "href": "https://pl.fediverse.pl/users/mkljczk",
+ "name": "@mkljczk@fediverse.pl",
+ "type": "Mention"
+ },
+ {
+ "href": "https://fedi.kutno.pl/users/mkljczk",
+ "name": "@mkljczk@fedi.kutno.pl",
+ "type": "Mention"
+ }
+ ],
+ "to": ["https://pol.social/users/mkljczk/followers"],
+ "type": "Note",
+ "updated": "2025-01-28T20:39:36Z",
+ "url": "https://pol.social/@mkljczk/113907871635572263"
+ },
+ "published": "2025-01-28T20:39:36Z",
+ "to": ["https://pol.social/users/mkljczk/followers"],
+ "type": "Update"
+}
diff --git a/test/fixtures/misskey-custom-emoji-like.json b/test/fixtures/misskey-custom-emoji-like.json
new file mode 100644
index 000000000..51a825d42
--- /dev/null
+++ b/test/fixtures/misskey-custom-emoji-like.json
@@ -0,0 +1,54 @@
+ {
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "Emoji": "toot:Emoji",
+ "Hashtag": "as:Hashtag",
+ "PropertyValue": "schema:PropertyValue",
+ "_misskey_content": "misskey:_misskey_content",
+ "_misskey_quote": "misskey:_misskey_quote",
+ "_misskey_reaction": "misskey:_misskey_reaction",
+ "_misskey_summary": "misskey:_misskey_summary",
+ "_misskey_votes": "misskey:_misskey_votes",
+ "backgroundUrl": "sharkey:backgroundUrl",
+ "discoverable": "toot:discoverable",
+ "featured": "toot:featured",
+ "fedibird": "http://fedibird.com/ns#",
+ "firefish": "https://joinfirefish.org/ns#",
+ "isCat": "misskey:isCat",
+ "listenbrainz": "sharkey:listenbrainz",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "misskey": "https://misskey-hub.net/ns#",
+ "quoteUri": "fedibird:quoteUri",
+ "quoteUrl": "as:quoteUrl",
+ "schema": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "sharkey": "https://joinsharkey.org/ns#",
+ "speakAsCat": "firefish:speakAsCat",
+ "toot": "http://joinmastodon.org/ns#",
+ "value": "schema:value",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "_misskey_reaction": ":blobwtfnotlikethis:",
+ "actor": "https://mai.waifuism.life/users/9otxaeemjqy70001",
+ "content": ":blobwtfnotlikethis:",
+ "id": "https://mai.waifuism.life/likes/9q2xifhrdnb0001b",
+ "object": "https://bungle.online/notes/9q2xi2sy4k",
+ "tag": [
+ {
+ "icon": {
+ "mediaType": "image/png",
+ "type": "Image",
+ "url": "https://mai.waifuism.life/files/1b0510f2-1fb4-43f5-a399-10053bbd8f0f"
+ },
+ "id": "https://mai.waifuism.life/emojis/blobwtfnotlikethis",
+ "name": ":blobwtfnotlikethis:",
+ "type": "Emoji",
+ "updated": "2024-02-07T02:21:46.497Z"
+ }
+ ],
+ "type": "Like"
+}
+
diff --git a/test/fixtures/mitra-custom-emoji-like.json b/test/fixtures/mitra-custom-emoji-like.json
new file mode 100644
index 000000000..4d727febd
--- /dev/null
+++ b/test/fixtures/mitra-custom-emoji-like.json
@@ -0,0 +1,46 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ "https://w3id.org/security/data-integrity/v1",
+ {
+ "Emoji": "toot:Emoji",
+ "Hashtag": "as:Hashtag",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#"
+ }
+ ],
+ "actor": "https://mitra.social/users/silverpill",
+ "cc": [],
+ "content": ":ablobcatheartsqueeze:",
+ "id": "https://mitra.social/activities/like/0195a89a-a3a0-ead4-3a1c-aa6311397cfd",
+ "object": "https://framapiaf.org/users/peertube/statuses/114182703352270287",
+ "proof": {
+ "created": "2025-03-18T09:34:21.610678375Z",
+ "cryptosuite": "eddsa-jcs-2022",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "z5AvpwkXQGFpTneRVDNeF48Jo9qYG6PgrE5HaPPpQNdNyc31ULMN4Vxd4aFXELo4Rk5Y9hd9nDy254xP8v5uGGWp1",
+ "type": "DataIntegrityProof",
+ "verificationMethod": "https://mitra.social/users/silverpill#ed25519-key"
+ },
+ "tag": [
+ {
+ "attributedTo": "https://mitra.social/actor",
+ "icon": {
+ "mediaType": "image/png",
+ "type": "Image",
+ "url": "https://mitra.social/media/a08e153441b25e512ab1b2e8922f5d8cd928322c8b79958cd48297ac722a4117.png"
+ },
+ "id": "https://mitra.social/objects/emojis/ablobcatheartsqueeze",
+ "name": ":ablobcatheartsqueeze:",
+ "type": "Emoji",
+ "updated": "1970-01-01T00:00:00Z"
+ }
+ ],
+ "to": [
+ "https://framapiaf.org/users/peertube",
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type": "Like"
+}
+
diff --git a/test/fixtures/rich_media/instagram_longtext.html b/test/fixtures/rich_media/instagram_longtext.html
new file mode 100644
index 000000000..e833f408c
--- /dev/null
+++ b/test/fixtures/rich_media/instagram_longtext.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+CAPTURE THE ATLAS | ✨ A Once-in-a-Lifetime Shot: Total Lunar Eclipse + Aurora Substorm! 🔴💚
+
+Last Thursday night, under the freezing skies of Northern Alaska, I... | Instagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/tesla_mock/deepl-languages-list.json b/test/fixtures/tesla_mock/deepl-languages-list.json
new file mode 100644
index 000000000..03d47d2ec
--- /dev/null
+++ b/test/fixtures/tesla_mock/deepl-languages-list.json
@@ -0,0 +1 @@
+[{"language":"BG","name":"Bulgarian","supports_formality":false},{"language":"CS","name":"Czech","supports_formality":false},{"language":"DA","name":"Danish","supports_formality":false},{"language":"DE","name":"German","supports_formality":true},{"language":"EL","name":"Greek","supports_formality":false},{"language":"EN-GB","name":"English (British)","supports_formality":false},{"language":"EN-US","name":"English (American)","supports_formality":false},{"language":"ES","name":"Spanish","supports_formality":true},{"language":"ET","name":"Estonian","supports_formality":false},{"language":"FI","name":"Finnish","supports_formality":false},{"language":"FR","name":"French","supports_formality":true},{"language":"HU","name":"Hungarian","supports_formality":false},{"language":"ID","name":"Indonesian","supports_formality":false},{"language":"IT","name":"Italian","supports_formality":true},{"language":"JA","name":"Japanese","supports_formality":false},{"language":"LT","name":"Lithuanian","supports_formality":false},{"language":"LV","name":"Latvian","supports_formality":false},{"language":"NL","name":"Dutch","supports_formality":true},{"language":"PL","name":"Polish","supports_formality":true},{"language":"PT-BR","name":"Portuguese (Brazilian)","supports_formality":true},{"language":"PT-PT","name":"Portuguese (European)","supports_formality":true},{"language":"RO","name":"Romanian","supports_formality":false},{"language":"RU","name":"Russian","supports_formality":true},{"language":"SK","name":"Slovak","supports_formality":false},{"language":"SL","name":"Slovenian","supports_formality":false},{"language":"SV","name":"Swedish","supports_formality":false},{"language":"TR","name":"Turkish","supports_formality":false},{"language":"UK","name":"Ukrainian","supports_formality":false},{"language":"ZH","name":"Chinese (simplified)","supports_formality":false}]
\ No newline at end of file
diff --git a/test/fixtures/tesla_mock/deepl-translation.json b/test/fixtures/tesla_mock/deepl-translation.json
new file mode 100644
index 000000000..fef7bb215
--- /dev/null
+++ b/test/fixtures/tesla_mock/deepl-translation.json
@@ -0,0 +1 @@
+{"translations":[{"detected_source_language":"PL","text":"REMOVE THE FOLLOWER!Paste this on your follower. If we get 70% of nk users...they will remove the follower!!!"}]}
\ No newline at end of file
diff --git a/test/fixtures/users_mock/friendica_followers.json b/test/fixtures/users_mock/friendica_followers.json
index 7b86b5fe2..f58c1d56c 100644
--- a/test/fixtures/users_mock/friendica_followers.json
+++ b/test/fixtures/users_mock/friendica_followers.json
@@ -13,7 +13,7 @@
"directMessage": "litepub:directMessage"
}
],
- "id": "http://localhost:8080/followers/fuser3",
+ "id": "https://remote.org/followers/fuser3",
"type": "OrderedCollection",
"totalItems": 296
}
diff --git a/test/fixtures/users_mock/friendica_following.json b/test/fixtures/users_mock/friendica_following.json
index 7c526befc..f3930f42c 100644
--- a/test/fixtures/users_mock/friendica_following.json
+++ b/test/fixtures/users_mock/friendica_following.json
@@ -13,7 +13,7 @@
"directMessage": "litepub:directMessage"
}
],
- "id": "http://localhost:8080/following/fuser3",
+ "id": "https://remote.org/following/fuser3",
"type": "OrderedCollection",
"totalItems": 32
}
diff --git a/test/fixtures/users_mock/masto_closed_followers.json b/test/fixtures/users_mock/masto_closed_followers.json
index da296892d..89bb9cba9 100644
--- a/test/fixtures/users_mock/masto_closed_followers.json
+++ b/test/fixtures/users_mock/masto_closed_followers.json
@@ -1,7 +1,7 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
- "id": "http://localhost:4001/users/masto_closed/followers",
+ "id": "https://remote.org/users/masto_closed/followers",
"type": "OrderedCollection",
"totalItems": 437,
- "first": "http://localhost:4001/users/masto_closed/followers?page=1"
+ "first": "https://remote.org/users/masto_closed/followers?page=1"
}
diff --git a/test/fixtures/users_mock/masto_closed_followers_page.json b/test/fixtures/users_mock/masto_closed_followers_page.json
index 04ab0c4d3..4e9cb315f 100644
--- a/test/fixtures/users_mock/masto_closed_followers_page.json
+++ b/test/fixtures/users_mock/masto_closed_followers_page.json
@@ -1 +1 @@
-{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"http://localhost:4001/users/masto_closed/followers?page=2","partOf":"http://localhost:4001/users/masto_closed/followers","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
+{"@context":"https://www.w3.org/ns/activitystreams","id":"https://remote.org/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"https://remote.org/users/masto_closed/followers?page=2","partOf":"https://remote.org/users/masto_closed/followers","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
diff --git a/test/fixtures/users_mock/masto_closed_following.json b/test/fixtures/users_mock/masto_closed_following.json
index 146d49f9c..aa74f8e78 100644
--- a/test/fixtures/users_mock/masto_closed_following.json
+++ b/test/fixtures/users_mock/masto_closed_following.json
@@ -1,7 +1,7 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
- "id": "http://localhost:4001/users/masto_closed/following",
+ "id": "https://remote.org/users/masto_closed/following",
"type": "OrderedCollection",
"totalItems": 152,
- "first": "http://localhost:4001/users/masto_closed/following?page=1"
+ "first": "https://remote.org/users/masto_closed/following?page=1"
}
diff --git a/test/fixtures/users_mock/masto_closed_following_page.json b/test/fixtures/users_mock/masto_closed_following_page.json
index 8d8324699..b017413cc 100644
--- a/test/fixtures/users_mock/masto_closed_following_page.json
+++ b/test/fixtures/users_mock/masto_closed_following_page.json
@@ -1 +1 @@
-{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"http://localhost:4001/users/masto_closed/following?page=2","partOf":"http://localhost:4001/users/masto_closed/following","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
+{"@context":"https://www.w3.org/ns/activitystreams","id":"https://remote.org/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"https://remote.org/users/masto_closed/following?page=2","partOf":"https://remote.org/users/masto_closed/following","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
diff --git a/test/fixtures/users_mock/pleroma_followers.json b/test/fixtures/users_mock/pleroma_followers.json
index db71d084b..6ac3bfee0 100644
--- a/test/fixtures/users_mock/pleroma_followers.json
+++ b/test/fixtures/users_mock/pleroma_followers.json
@@ -1,18 +1,18 @@
{
"type": "OrderedCollection",
"totalItems": 527,
- "id": "http://localhost:4001/users/fuser2/followers",
+ "id": "https://remote.org/users/fuser2/followers",
"first": {
"type": "OrderedCollectionPage",
"totalItems": 527,
- "partOf": "http://localhost:4001/users/fuser2/followers",
+ "partOf": "https://remote.org/users/fuser2/followers",
"orderedItems": [],
- "next": "http://localhost:4001/users/fuser2/followers?page=2",
- "id": "http://localhost:4001/users/fuser2/followers?page=1"
+ "next": "https://remote.org/users/fuser2/followers?page=2",
+ "id": "https://remote.org/users/fuser2/followers?page=1"
},
"@context": [
"https://www.w3.org/ns/activitystreams",
- "http://localhost:4001/schemas/litepub-0.1.jsonld",
+ "https://remote.org/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
diff --git a/test/fixtures/users_mock/pleroma_following.json b/test/fixtures/users_mock/pleroma_following.json
index 33d087703..c8306806a 100644
--- a/test/fixtures/users_mock/pleroma_following.json
+++ b/test/fixtures/users_mock/pleroma_following.json
@@ -1,18 +1,18 @@
{
"type": "OrderedCollection",
"totalItems": 267,
- "id": "http://localhost:4001/users/fuser2/following",
+ "id": "https://remote.org/users/fuser2/following",
"first": {
"type": "OrderedCollectionPage",
"totalItems": 267,
- "partOf": "http://localhost:4001/users/fuser2/following",
+ "partOf": "https://remote.org/users/fuser2/following",
"orderedItems": [],
- "next": "http://localhost:4001/users/fuser2/following?page=2",
- "id": "http://localhost:4001/users/fuser2/following?page=1"
+ "next": "https://remote.org/users/fuser2/following?page=2",
+ "id": "https://remote.org/users/fuser2/following?page=1"
},
"@context": [
"https://www.w3.org/ns/activitystreams",
- "http://localhost:4001/schemas/litepub-0.1.jsonld",
+ "https://remote.org/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
diff --git a/test/fixtures/warnings/otp_version/21.1 b/test/fixtures/warnings/otp_version/21.1
deleted file mode 100644
index 90cd64c4f..000000000
--- a/test/fixtures/warnings/otp_version/21.1
+++ /dev/null
@@ -1 +0,0 @@
-21.1
\ No newline at end of file
diff --git a/test/fixtures/warnings/otp_version/22.1 b/test/fixtures/warnings/otp_version/22.1
deleted file mode 100644
index d9b314368..000000000
--- a/test/fixtures/warnings/otp_version/22.1
+++ /dev/null
@@ -1 +0,0 @@
-22.1
\ No newline at end of file
diff --git a/test/fixtures/warnings/otp_version/22.4 b/test/fixtures/warnings/otp_version/22.4
deleted file mode 100644
index 1da8ccd28..000000000
--- a/test/fixtures/warnings/otp_version/22.4
+++ /dev/null
@@ -1 +0,0 @@
-22.4
\ No newline at end of file
diff --git a/test/fixtures/warnings/otp_version/23.0 b/test/fixtures/warnings/otp_version/23.0
deleted file mode 100644
index 4266d8634..000000000
--- a/test/fixtures/warnings/otp_version/23.0
+++ /dev/null
@@ -1 +0,0 @@
-23.0
\ No newline at end of file
diff --git a/test/mix/tasks/pleroma/app_test.exs b/test/mix/tasks/pleroma/app_test.exs
index f35447edc..65245eadd 100644
--- a/test/mix/tasks/pleroma/app_test.exs
+++ b/test/mix/tasks/pleroma/app_test.exs
@@ -42,9 +42,10 @@ defmodule Mix.Tasks.Pleroma.AppTest do
test "with errors" do
Mix.Tasks.Pleroma.App.run(["create"])
- {:mix_shell, :error, ["Creating failed:"]}
- {:mix_shell, :error, ["name: can't be blank"]}
- {:mix_shell, :error, ["redirect_uris: can't be blank"]}
+
+ assert_receive {:mix_shell, :error, ["Creating failed:"]}
+ assert_receive {:mix_shell, :error, ["name: can't be blank"]}
+ assert_receive {:mix_shell, :error, ["redirect_uris: can't be blank"]}
end
defp assert_app(name, redirect, scopes) do
diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs
index 96a925528..38ed096ae 100644
--- a/test/mix/tasks/pleroma/database_test.exs
+++ b/test/mix/tasks/pleroma/database_test.exs
@@ -411,7 +411,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
["scheduled_activities"],
["schema_migrations"],
["thread_mutes"],
- # ["user_follows_hashtag"], # not in pleroma
+ ["user_follows_hashtag"],
# ["user_frontend_setting_profiles"], # not in pleroma
["user_invite_tokens"],
["user_notes"],
diff --git a/test/mix/tasks/pleroma/digest_test.exs b/test/mix/tasks/pleroma/digest_test.exs
index 08482aadb..0d1804cdb 100644
--- a/test/mix/tasks/pleroma/digest_test.exs
+++ b/test/mix/tasks/pleroma/digest_test.exs
@@ -24,7 +24,7 @@ defmodule Mix.Tasks.Pleroma.DigestTest do
setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true)
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/mix/tasks/pleroma/frontend_test.exs b/test/mix/tasks/pleroma/frontend_test.exs
index 6d09f8e36..59ebcec92 100644
--- a/test/mix/tasks/pleroma/frontend_test.exs
+++ b/test/mix/tasks/pleroma/frontend_test.exs
@@ -11,7 +11,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do
@dir "test/frontend_static_test"
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
clear_config([:instance, :static_dir], @dir)
on_exit(fn ->
@@ -50,7 +50,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do
folder = Path.join([@dir, "frontends", "pleroma", "fantasy"])
previously_existing = Path.join([folder, "temp"])
- File.mkdir_p!(folder)
+ Pleroma.Backports.mkdir_p!(folder)
File.write!(previously_existing, "yey")
assert File.exists?(previously_existing)
diff --git a/test/mix/tasks/pleroma/instance_test.exs b/test/mix/tasks/pleroma/instance_test.exs
index b1c10e03c..5ecb6e445 100644
--- a/test/mix/tasks/pleroma/instance_test.exs
+++ b/test/mix/tasks/pleroma/instance_test.exs
@@ -7,7 +7,7 @@ defmodule Mix.Tasks.Pleroma.InstanceTest do
use Pleroma.DataCase
setup do
- File.mkdir_p!(tmp_path())
+ Pleroma.Backports.mkdir_p!(tmp_path())
on_exit(fn ->
File.rm_rf(tmp_path())
diff --git a/test/mix/tasks/pleroma/uploads_test.exs b/test/mix/tasks/pleroma/uploads_test.exs
index f3d5aa64f..0aa24807e 100644
--- a/test/mix/tasks/pleroma/uploads_test.exs
+++ b/test/mix/tasks/pleroma/uploads_test.exs
@@ -62,7 +62,7 @@ defmodule Mix.Tasks.Pleroma.UploadsTest do
upload_dir = Config.get([Pleroma.Uploaders.Local, :uploads])
if not File.exists?(upload_dir) || File.ls!(upload_dir) == [] do
- File.mkdir_p(upload_dir)
+ Pleroma.Backports.mkdir_p(upload_dir)
Path.join([upload_dir, "file.txt"])
|> File.touch()
diff --git a/test/mix/tasks/pleroma/user_test.exs b/test/mix/tasks/pleroma/user_test.exs
index c9bcf2951..7ce5e92cb 100644
--- a/test/mix/tasks/pleroma/user_test.exs
+++ b/test/mix/tasks/pleroma/user_test.exs
@@ -21,7 +21,7 @@ defmodule Mix.Tasks.Pleroma.UserTest do
import Pleroma.Factory
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/conversation_test.exs b/test/pleroma/conversation_test.exs
index 809c1951a..02b5de615 100644
--- a/test/pleroma/conversation_test.exs
+++ b/test/pleroma/conversation_test.exs
@@ -14,7 +14,7 @@ defmodule Pleroma.ConversationTest do
setup_all do: clear_config([:instance, :federating], true)
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/content_language_map_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/content_language_map_test.exs
new file mode 100644
index 000000000..a05871a6f
--- /dev/null
+++ b/test/pleroma/ecto_type/activity_pub/object_validators/content_language_map_test.exs
@@ -0,0 +1,56 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMapTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap
+
+ test "it validates" do
+ data = %{
+ "en-US" => "mew mew",
+ "en-GB" => "meow meow"
+ }
+
+ assert {:ok, ^data} = ContentLanguageMap.cast(data)
+ end
+
+ test "it validates empty strings" do
+ data = %{
+ "en-US" => "mew mew",
+ "en-GB" => ""
+ }
+
+ assert {:ok, ^data} = ContentLanguageMap.cast(data)
+ end
+
+ test "it ignores non-strings within the map" do
+ data = %{
+ "en-US" => "mew mew",
+ "en-GB" => 123
+ }
+
+ assert {:ok, validated_data} = ContentLanguageMap.cast(data)
+
+ assert validated_data == %{"en-US" => "mew mew"}
+ end
+
+ test "it ignores bad locale codes" do
+ data = %{
+ "en-US" => "mew mew",
+ "en_GB" => "meow meow",
+ "en<<#@!$#!@%!GB" => "meow meow"
+ }
+
+ assert {:ok, validated_data} = ContentLanguageMap.cast(data)
+
+ assert validated_data == %{"en-US" => "mew mew"}
+ end
+
+ test "it complains with non-map data" do
+ assert :error = ContentLanguageMap.cast("mew")
+ assert :error = ContentLanguageMap.cast(["mew"])
+ assert :error = ContentLanguageMap.cast([%{"en-US" => "mew"}])
+ end
+end
diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/language_code_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/language_code_test.exs
new file mode 100644
index 000000000..086bb3e97
--- /dev/null
+++ b/test/pleroma/ecto_type/activity_pub/object_validators/language_code_test.exs
@@ -0,0 +1,29 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCodeTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode
+
+ test "it accepts language code" do
+ text = "pl"
+ assert {:ok, ^text} = LanguageCode.cast(text)
+ end
+
+ test "it accepts language code with region" do
+ text = "pl-PL"
+ assert {:ok, ^text} = LanguageCode.cast(text)
+ end
+
+ test "errors for invalid language code" do
+ assert {:error, :invalid_language} = LanguageCode.cast("ru_RU")
+ assert {:error, :invalid_language} = LanguageCode.cast(" ")
+ assert {:error, :invalid_language} = LanguageCode.cast("en-US\n")
+ end
+
+ test "errors for non-text" do
+ assert :error == LanguageCode.cast(42)
+ end
+end
diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs
index 00001abfc..56b17ac31 100644
--- a/test/pleroma/emoji/pack_test.exs
+++ b/test/pleroma/emoji/pack_test.exs
@@ -4,6 +4,7 @@
defmodule Pleroma.Emoji.PackTest do
use Pleroma.DataCase
+ alias Pleroma.Emoji
alias Pleroma.Emoji.Pack
@emoji_path Path.join(
@@ -12,6 +13,9 @@ defmodule Pleroma.Emoji.PackTest do
)
setup do
+ # Reload emoji to ensure a clean state
+ Emoji.reload()
+
pack_path = Path.join(@emoji_path, "dump_pack")
File.mkdir(pack_path)
@@ -53,6 +57,63 @@ defmodule Pleroma.Emoji.PackTest do
assert updated_pack.files_count == 5
end
+
+ test "skips existing emojis when adding from zip file", %{pack: pack} do
+ # First, let's create a test pack with a "bear" emoji
+ test_pack_path = Path.join(@emoji_path, "test_bear_pack")
+ Pleroma.Backports.mkdir_p(test_pack_path)
+
+ # Create a pack.json file
+ File.write!(Path.join(test_pack_path, "pack.json"), """
+ {
+ "files": { "bear": "bear.png" },
+ "pack": {
+ "description": "Bear Pack", "homepage": "https://pleroma.social",
+ "license": "Test license", "share-files": true
+ }}
+ """)
+
+ # Copy a test image to use as the bear emoji
+ File.cp!(
+ Path.absname("test/instance_static/emoji/test_pack/blank.png"),
+ Path.join(test_pack_path, "bear.png")
+ )
+
+ # Load the pack to register the "bear" emoji in the global registry
+ {:ok, _bear_pack} = Pleroma.Emoji.Pack.load_pack("test_bear_pack")
+
+ # Reload emoji to make sure the bear emoji is in the global registry
+ Emoji.reload()
+
+ # Verify that the bear emoji exists in the global registry
+ assert Emoji.exist?("bear")
+
+ # Now try to add a zip file that contains an emoji with the same shortcode
+ file = %Plug.Upload{
+ content_type: "application/zip",
+ filename: "emojis.zip",
+ path: Path.absname("test/fixtures/emojis.zip")
+ }
+
+ {:ok, updated_pack} = Pack.add_file(pack, nil, nil, file)
+
+ # Verify that the "bear" emoji was skipped
+ refute Map.has_key?(updated_pack.files, "bear")
+
+ # Other emojis should be added
+ assert Map.has_key?(updated_pack.files, "a_trusted_friend-128")
+ assert Map.has_key?(updated_pack.files, "auroraborealis")
+ assert Map.has_key?(updated_pack.files, "baby_in_a_box")
+ assert Map.has_key?(updated_pack.files, "bear-128")
+
+ # Total count should be 4 (all emojis except "bear")
+ assert updated_pack.files_count == 4
+
+ # Clean up the test pack
+ on_exit(fn ->
+ File.rm_rf!(test_pack_path)
+ end)
+ end
end
test "returns error when zip file is bad", %{pack: pack} do
@@ -62,7 +123,7 @@ defmodule Pleroma.Emoji.PackTest do
path: Path.absname("test/instance_static/emoji/test_pack/blank.png")
}
- assert Pack.add_file(pack, nil, nil, file) == {:error, :einval}
+ assert {:error, _} = Pack.add_file(pack, nil, nil, file)
end
test "returns pack when zip file is empty", %{pack: pack} do
diff --git a/test/pleroma/frontend_test.exs b/test/pleroma/frontend_test.exs
index c89c56c8c..22e0ffb9a 100644
--- a/test/pleroma/frontend_test.exs
+++ b/test/pleroma/frontend_test.exs
@@ -9,7 +9,7 @@ defmodule Pleroma.FrontendTest do
@dir "test/frontend_static_test"
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
clear_config([:instance, :static_dir], @dir)
on_exit(fn ->
@@ -46,7 +46,7 @@ defmodule Pleroma.FrontendTest do
folder = Path.join([@dir, "frontends", "pleroma", "fantasy"])
previously_existing = Path.join([folder, "temp"])
- File.mkdir_p!(folder)
+ Pleroma.Backports.mkdir_p!(folder)
File.write!(previously_existing, "yey")
assert File.exists?(previously_existing)
diff --git a/test/pleroma/hashtag_test.exs b/test/pleroma/hashtag_test.exs
index 8531b1879..0e16b8155 100644
--- a/test/pleroma/hashtag_test.exs
+++ b/test/pleroma/hashtag_test.exs
@@ -14,4 +14,133 @@ defmodule Pleroma.HashtagTest do
assert {:name, {"can't be blank", [validation: :required]}} in changeset.errors
end
end
+
+ describe "search_hashtags" do
+ test "searches hashtags by partial match" do
+ {:ok, _} = Hashtag.get_or_create_by_name("car")
+ {:ok, _} = Hashtag.get_or_create_by_name("racecar")
+ {:ok, _} = Hashtag.get_or_create_by_name("nascar")
+ {:ok, _} = Hashtag.get_or_create_by_name("bicycle")
+
+ results = Hashtag.search("car")
+ assert "car" in results
+ assert "racecar" in results
+ assert "nascar" in results
+ refute "bicycle" in results
+
+ results = Hashtag.search("race")
+ assert "racecar" in results
+ refute "car" in results
+ refute "nascar" in results
+ refute "bicycle" in results
+
+ results = Hashtag.search("nonexistent")
+ assert results == []
+ end
+
+ test "searches hashtags by multiple words in query" do
+ {:ok, _} = Hashtag.get_or_create_by_name("computer")
+ {:ok, _} = Hashtag.get_or_create_by_name("laptop")
+ {:ok, _} = Hashtag.get_or_create_by_name("desktop")
+ {:ok, _} = Hashtag.get_or_create_by_name("phone")
+
+ # Search for "new computer" - should return "computer"
+ results = Hashtag.search("new computer")
+ assert "computer" in results
+ refute "laptop" in results
+ refute "desktop" in results
+ refute "phone" in results
+
+ # Search for "computer laptop" - should return both
+ results = Hashtag.search("computer laptop")
+ assert "computer" in results
+ assert "laptop" in results
+ refute "desktop" in results
+ refute "phone" in results
+
+ # Search for "new phone" - should return "phone"
+ results = Hashtag.search("new phone")
+ assert "phone" in results
+ refute "computer" in results
+ refute "laptop" in results
+ refute "desktop" in results
+ end
+
+ test "supports pagination" do
+ {:ok, _} = Hashtag.get_or_create_by_name("alpha")
+ {:ok, _} = Hashtag.get_or_create_by_name("beta")
+ {:ok, _} = Hashtag.get_or_create_by_name("gamma")
+ {:ok, _} = Hashtag.get_or_create_by_name("delta")
+
+ results = Hashtag.search("a", limit: 2)
+ assert length(results) == 2
+
+ results = Hashtag.search("a", limit: 2, offset: 1)
+ assert length(results) == 2
+ end
+
+ test "handles matching many search terms" do
+ {:ok, _} = Hashtag.get_or_create_by_name("computer")
+ {:ok, _} = Hashtag.get_or_create_by_name("laptop")
+ {:ok, _} = Hashtag.get_or_create_by_name("phone")
+ {:ok, _} = Hashtag.get_or_create_by_name("tablet")
+
+ results = Hashtag.search("new fast computer laptop phone tablet device")
+ assert "computer" in results
+ assert "laptop" in results
+ assert "phone" in results
+ assert "tablet" in results
+ end
+
+ test "ranks results by match quality" do
+ {:ok, _} = Hashtag.get_or_create_by_name("my_computer")
+ {:ok, _} = Hashtag.get_or_create_by_name("computer_science")
+ {:ok, _} = Hashtag.get_or_create_by_name("computer")
+
+ results = Hashtag.search("computer")
+
+ # Exact match first
+ assert Enum.at(results, 0) == "computer"
+
+ # Prefix match would be next
+ assert Enum.at(results, 1) == "computer_science"
+
+ # worst match is last
+ assert Enum.at(results, 2) == "my_computer"
+ end
+
+ test "prioritizes shorter names when ranking is equal" do
+ # Create hashtags with same ranking but different lengths
+ {:ok, _} = Hashtag.get_or_create_by_name("car")
+ {:ok, _} = Hashtag.get_or_create_by_name("racecar")
+ {:ok, _} = Hashtag.get_or_create_by_name("nascar")
+
+ # Search for "car" - shorter names should come first
+ results = Hashtag.search("car")
+ # Shortest exact match first
+ assert Enum.at(results, 0) == "car"
+ assert "racecar" in results
+ assert "nascar" in results
+ end
+
+ test "handles hashtag symbols in search query" do
+ {:ok, _} = Hashtag.get_or_create_by_name("computer")
+ {:ok, _} = Hashtag.get_or_create_by_name("laptop")
+ {:ok, _} = Hashtag.get_or_create_by_name("phone")
+
+ results_with_hash = Hashtag.search("#computer #laptop")
+ results_without_hash = Hashtag.search("computer laptop")
+
+ assert results_with_hash == results_without_hash
+
+ results_mixed = Hashtag.search("#computer laptop #phone")
+ assert "computer" in results_mixed
+ assert "laptop" in results_mixed
+ assert "phone" in results_mixed
+
+ results_only_hash = Hashtag.search("#computer")
+ results_no_hash = Hashtag.search("computer")
+ assert results_only_hash == results_no_hash
+ end
+ end
end
diff --git a/test/pleroma/http_test.exs b/test/pleroma/http_test.exs
index de359e599..61347015d 100644
--- a/test/pleroma/http_test.exs
+++ b/test/pleroma/http_test.exs
@@ -25,6 +25,9 @@ defmodule Pleroma.HTTPTest do
%{method: :post, url: "http://example.com/world"} ->
%Tesla.Env{status: 200, body: "world"}
+
+ %{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} ->
+ %Tesla.Env{status: 200, body: "emoji data"}
end)
:ok
@@ -67,4 +70,20 @@ defmodule Pleroma.HTTPTest do
}
end
end
+
+ test "URL encoding properly encodes URLs with spaces" do
+ clear_config(:test_url_encoding, true)
+
+ url_with_space = "https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz"
+
+ {:ok, result} = HTTP.get(url_with_space)
+
+ assert result.status == 200
+
+ properly_encoded_url = "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"
+
+ {:ok, result} = HTTP.get(properly_encoded_url)
+
+ assert result.status == 200
+ end
end
diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs
index 6a718be21..bc3e7993e 100644
--- a/test/pleroma/instances/instance_test.exs
+++ b/test/pleroma/instances/instance_test.exs
@@ -3,19 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances.InstanceTest do
- alias Pleroma.Instances
alias Pleroma.Instances.Instance
alias Pleroma.Repo
- alias Pleroma.Tests.ObanHelpers
- alias Pleroma.Web.CommonAPI
+ use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
import ExUnit.CaptureLog
import Pleroma.Factory
- setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
-
describe "set_reachable/1" do
test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do
unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
@@ -31,6 +27,32 @@ defmodule Pleroma.Instances.InstanceTest do
assert {:ok, instance} = Instance.set_reachable(instance.host)
refute instance.unreachable_since
end
+
+ test "cancels all ReachabilityWorker jobs for the domain" do
+ domain = "cancelme.example.org"
+ insert(:instance, host: domain, unreachable_since: NaiveDateTime.utc_now())
+
+ # Insert a ReachabilityWorker job for this domain, scheduled 5 minutes in the future
+ scheduled_at = DateTime.add(DateTime.utc_now(), 300, :second)
+
+ {:ok, job} =
+ Pleroma.Workers.ReachabilityWorker.new(
+ %{"domain" => domain, "phase" => "phase_1min", "attempt" => 1},
+ scheduled_at: scheduled_at
+ )
+ |> Oban.insert()
+
+ # Ensure the job is present
+ job = Pleroma.Repo.get(Oban.Job, job.id)
+ assert job
+
+ # Call set_reachable, which should delete the job
+ assert {:ok, _} = Instance.set_reachable(domain)
+
+ # Reload the job and assert it is deleted
+ job = Pleroma.Repo.get(Oban.Job, job.id)
+ refute job
+ end
end
describe "set_unreachable/1" do
@@ -145,7 +167,11 @@ defmodule Pleroma.Instances.InstanceTest do
end
test "Doesn't scrapes unreachable instances" do
- instance = insert(:instance, unreachable_since: Instances.reachability_datetime_threshold())
+ instance =
+ insert(:instance,
+ unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.add(-:timer.hours(24))
+ )
+
url = "https://" <> instance.host
assert capture_log(fn -> assert nil == Instance.get_or_update_favicon(URI.parse(url)) end) =~
@@ -213,32 +239,44 @@ defmodule Pleroma.Instances.InstanceTest do
end
end
- test "delete_users_and_activities/1 deletes remote instance users and activities" do
- [mario, luigi, _peach, wario] =
- users = [
- insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario"),
- insert(:user, nickname: "luigi@mushroom.kingdom", name: "Luigi"),
- insert(:user, nickname: "peach@mushroom.kingdom", name: "Peach"),
- insert(:user, nickname: "wario@greedville.biz", name: "Wario")
- ]
+ test "delete/1 schedules a job to delete the instance and users" do
+ insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario")
- {:ok, post1} = CommonAPI.post(mario, %{status: "letsa go!"})
- {:ok, post2} = CommonAPI.post(luigi, %{status: "itsa me... luigi"})
- {:ok, post3} = CommonAPI.post(wario, %{status: "WHA-HA-HA!"})
+ {:ok, _job} = Instance.delete("mushroom.kingdom")
- {:ok, job} = Instance.delete_users_and_activities("mushroom.kingdom")
- :ok = ObanHelpers.perform(job)
+ assert_enqueued(
+ worker: Pleroma.Workers.DeleteWorker,
+ args: %{"op" => "delete_instance", "host" => "mushroom.kingdom"}
+ )
+ end
- [mario, luigi, peach, wario] = Repo.reload(users)
+ describe "check_unreachable/1" do
+ test "schedules a ReachabilityWorker job for the given domain" do
+ domain = "test.example.com"
- refute mario.is_active
- refute luigi.is_active
- refute peach.is_active
- refute peach.name == "Peach"
+ # Call check_unreachable
+ assert {:ok, _job} = Instance.check_unreachable(domain)
- assert wario.is_active
- assert wario.name == "Wario"
+ # Verify that a ReachabilityWorker job was scheduled
+ jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
+ assert length(jobs) == 1
+ [job] = jobs
+ assert job.args["domain"] == domain
+ end
- assert [nil, nil, %{}] = Repo.reload([post1, post2, post3])
+ test "handles multiple calls for the same domain (uniqueness enforced)" do
+ domain = "duplicate.example.com"
+
+ assert {:ok, _job1} = Instance.check_unreachable(domain)
+
+ # Second call for the same domain
+ assert {:ok, %Oban.Job{conflict?: true}} = Instance.check_unreachable(domain)
+
+ # Should only have one job due to uniqueness
+ jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
+ assert length(jobs) == 1
+ [job] = jobs
+ assert job.args["domain"] == domain
+ end
end
end
diff --git a/test/pleroma/instances_test.exs b/test/pleroma/instances_test.exs
index 96fa9cffe..c8618b748 100644
--- a/test/pleroma/instances_test.exs
+++ b/test/pleroma/instances_test.exs
@@ -6,74 +6,42 @@ defmodule Pleroma.InstancesTest do
alias Pleroma.Instances
use Pleroma.DataCase
-
- setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
+ use Oban.Testing, repo: Pleroma.Repo
describe "reachable?/1" do
test "returns `true` for host / url with unknown reachability status" do
assert Instances.reachable?("unknown.site")
assert Instances.reachable?("http://unknown.site")
end
-
- test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do
- host = "consistently-unreachable.name"
- Instances.set_consistently_unreachable(host)
-
- refute Instances.reachable?(host)
- refute Instances.reachable?("http://#{host}/path")
- end
-
- test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do
- url = "http://eventually-unreachable.name/path"
-
- Instances.set_unreachable(url)
-
- assert Instances.reachable?(url)
- assert Instances.reachable?(URI.parse(url).host)
- end
-
- test "raises FunctionClauseError exception on non-binary input" do
- assert_raise FunctionClauseError, fn -> Instances.reachable?(nil) end
- assert_raise FunctionClauseError, fn -> Instances.reachable?(1) end
- end
end
describe "filter_reachable/1" do
setup do
- host = "consistently-unreachable.name"
- url1 = "http://eventually-unreachable.com/path"
- url2 = "http://domain.com/path"
+ unreachable_host = "consistently-unreachable.name"
+ reachable_host = "http://domain.com/path"
- Instances.set_consistently_unreachable(host)
- Instances.set_unreachable(url1)
+ Instances.set_unreachable(unreachable_host)
- result = Instances.filter_reachable([host, url1, url2, nil])
- %{result: result, url1: url1, url2: url2}
+ result = Instances.filter_reachable([unreachable_host, reachable_host, nil])
+ %{result: result, reachable_host: reachable_host, unreachable_host: unreachable_host}
end
- test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list",
- %{result: result, url1: url1, url2: url2} do
- assert is_map(result)
- assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort()
+ test "returns a list of only reachable elements",
+ %{result: result, reachable_host: reachable_host} do
+ assert is_list(result)
+ assert [reachable_host] == result
end
- test "returns a map with `unreachable_since` values for keys",
- %{result: result, url1: url1, url2: url2} do
- assert is_map(result)
- assert %NaiveDateTime{} = result[url1]
- assert is_nil(result[url2])
- end
-
- test "returns an empty map for empty list or list containing no hosts / url" do
- assert %{} == Instances.filter_reachable([])
- assert %{} == Instances.filter_reachable([nil])
+ test "returns an empty list when provided no data" do
+ assert [] == Instances.filter_reachable([])
+ assert [] == Instances.filter_reachable([nil])
end
end
describe "set_reachable/1" do
test "sets unreachable url or host reachable" do
host = "domain.com"
- Instances.set_consistently_unreachable(host)
+ Instances.set_unreachable(host)
refute Instances.reachable?(host)
Instances.set_reachable(host)
@@ -103,22 +71,68 @@ defmodule Pleroma.InstancesTest do
end
end
- describe "set_consistently_unreachable/1" do
- test "sets reachable url or host unreachable" do
- url = "http://domain.com?q="
- assert Instances.reachable?(url)
+ describe "check_all_unreachable/0" do
+ test "schedules ReachabilityWorker jobs for all unreachable instances" do
+ domain1 = "unreachable1.example.com"
+ domain2 = "unreachable2.example.com"
+ domain3 = "unreachable3.example.com"
- Instances.set_consistently_unreachable(url)
- refute Instances.reachable?(url)
+ Instances.set_unreachable(domain1)
+ Instances.set_unreachable(domain2)
+ Instances.set_unreachable(domain3)
+
+ Instances.check_all_unreachable()
+
+ # Verify that ReachabilityWorker jobs were scheduled for all unreachable domains
+ jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
+ assert length(jobs) == 3
+
+ domains = Enum.map(jobs, & &1.args["domain"])
+ assert domain1 in domains
+ assert domain2 in domains
+ assert domain3 in domains
end
- test "keeps unreachable url or host unreachable" do
- host = "site.name"
- Instances.set_consistently_unreachable(host)
- refute Instances.reachable?(host)
+ test "does not schedule jobs for reachable instances" do
+ unreachable_domain = "unreachable.example.com"
+ reachable_domain = "reachable.example.com"
- Instances.set_consistently_unreachable(host)
- refute Instances.reachable?(host)
+ Instances.set_unreachable(unreachable_domain)
+ Instances.set_reachable(reachable_domain)
+
+ Instances.check_all_unreachable()
+
+ # Verify that only one job was scheduled (for the unreachable domain)
+ jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
+ assert length(jobs) == 1
+ [job] = jobs
+ assert job.args["domain"] == unreachable_domain
end
end
+
+ test "delete_all_unreachable/0 schedules DeleteWorker jobs for all unreachable instances" do
+ domain1 = "unreachable1.example.com"
+ domain2 = "unreachable2.example.com"
+ domain3 = "unreachable3.example.com"
+
+ Instances.set_unreachable(domain1)
+ Instances.set_unreachable(domain2)
+ Instances.set_unreachable(domain3)
+
+ Instances.delete_all_unreachable()
+
+ # Verify that DeleteWorker jobs were scheduled for all unreachable domains
+ jobs = all_enqueued(worker: Pleroma.Workers.DeleteWorker)
+ assert length(jobs) == 3
+
+ domains = Enum.map(jobs, & &1.args["host"])
+ assert domain1 in domains
+ assert domain2 in domains
+ assert domain3 in domains
+
+ # Verify all jobs are delete_instance operations
+ Enum.each(jobs, fn job ->
+ assert job.args["op"] == "delete_instance"
+ end)
+ end
end
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index f499f54ad..88f32762d 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -268,6 +268,17 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
end)
end
+ test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
+ assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
+
+ capture_log(fn ->
+ assert {:error, %WebSockex.RequestError{code: 401}} =
+ start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
+
+ Process.sleep(30)
+ end)
+ end
+
test "accepts valid token on client-sent event", %{token: token} do
assert {:ok, pid} = start_socket()
diff --git a/test/pleroma/language/language_detector_test.exs b/test/pleroma/language/language_detector_test.exs
new file mode 100644
index 000000000..ccb81d5bd
--- /dev/null
+++ b/test/pleroma/language/language_detector_test.exs
@@ -0,0 +1,56 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Language.LanguageDetectorTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Language.LanguageDetector
+ alias Pleroma.Language.LanguageDetectorMock
+ alias Pleroma.StaticStubbedConfigMock
+
+ import Mox
+
+ setup do
+ # Stub the StaticStubbedConfigMock to return our mock for the provider
+ StaticStubbedConfigMock
+ |> stub(:get, fn
+ [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
+ _other -> nil
+ end)
+
+ # Stub the LanguageDetectorMock with default implementations
+ LanguageDetectorMock
+ |> stub(:missing_dependencies, fn -> [] end)
+ |> stub(:configured?, fn -> true end)
+
+ :ok
+ end
+
+ test "it detects text language" do
+ LanguageDetectorMock
+ |> expect(:detect, fn _text -> "fr" end)
+
+ detected_language = LanguageDetector.detect("Je viens d'atterrir en Tchéquie.")
+
+ assert detected_language == "fr"
+ end
+
+ test "it returns nil if text is not long enough" do
+ # No need to set expectations as the word count check happens before the provider is called
+
+ detected_language = LanguageDetector.detect("it returns nil")
+
+ assert detected_language == nil
+ end
+
+ test "it returns nil if no provider specified" do
+ # Override the stub to return nil for the provider
+ StaticStubbedConfigMock
+ |> expect(:get, fn [Pleroma.Language.LanguageDetector, :provider] -> nil end)
+
+ detected_language = LanguageDetector.detect("this should also return nil")
+
+ assert detected_language == nil
+ end
+end
diff --git a/test/pleroma/language/translation/deepl_test.exs b/test/pleroma/language/translation/deepl_test.exs
new file mode 100644
index 000000000..3a7265622
--- /dev/null
+++ b/test/pleroma/language/translation/deepl_test.exs
@@ -0,0 +1,37 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Language.Translation.DeeplTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Language.Translation.Deepl
+
+ test "it translates text" do
+ Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
+ clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
+
+ {:ok, res} =
+ Deepl.translate(
+ "USUNĄĆ ŚLEDZIKA!Wklej to na swojego śledzika. Jeżeli uzbieramy 70% użytkowników nk...to usuną śledzika!!!",
+ "pl",
+ "en"
+ )
+
+ assert %{
+ detected_source_language: "PL",
+ provider: "DeepL"
+ } = res
+ end
+
+ test "it returns languages list" do
+ Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
+ clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
+
+ assert {:ok, [language | _languages]} = Deepl.supported_languages(:target)
+
+ assert is_binary(language)
+ end
+end
diff --git a/test/pleroma/language/translation_test.exs b/test/pleroma/language/translation_test.exs
new file mode 100644
index 000000000..0be7a8d60
--- /dev/null
+++ b/test/pleroma/language/translation_test.exs
@@ -0,0 +1,28 @@
+defmodule Pleroma.Language.TranslationTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Language.Translation
+
+ setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
+
+ test "it translates text" do
+ assert {:ok,
+ %{
+ content: "txet emos",
+ detected_source_language: _,
+ provider: _
+ }} = Translation.translate("some text", "en", "uk")
+ end
+
+ test "it stores translation result in cache" do
+ Translation.translate("some text", "en", "uk")
+
+ assert {:ok, result} =
+ Cachex.get(
+ :translations_cache,
+ "en/uk/#{:crypto.hash(:sha256, "some text") |> Base.encode64()}"
+ )
+
+ assert result.content == "txet emos"
+ end
+end
diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs
index e595c5c53..4b20e07cf 100644
--- a/test/pleroma/notification_test.exs
+++ b/test/pleroma/notification_test.exs
@@ -19,7 +19,7 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.Web.MastodonAPI.NotificationView
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs
index 215fca570..9afa34fa2 100644
--- a/test/pleroma/object/fetcher_test.exs
+++ b/test/pleroma/object/fetcher_test.exs
@@ -6,7 +6,6 @@ defmodule Pleroma.Object.FetcherTest do
use Pleroma.DataCase
alias Pleroma.Activity
- alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Web.ActivityPub.ObjectValidator
@@ -166,15 +165,89 @@ defmodule Pleroma.Object.FetcherTest do
)
end
- test "it resets instance reachability on successful fetch" do
- id = "http://mastodon.example.org/@admin/99541947525187367"
- Instances.set_consistently_unreachable(id)
- refute Instances.reachable?(id)
+ test "it does not fetch from local instance" do
+ local_url = Pleroma.Web.Endpoint.url() <> "/objects/local_resource"
- {:ok, _object} =
- Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
+ assert {:fetch, {:error, "Trying to fetch local resource"}} =
+ Fetcher.fetch_object_from_id(local_url)
+ end
- assert Instances.reachable?(id)
+ test "it validates content-type headers according to ActivityPub spec" do
+ # Setup a mock for an object with invalid content-type
+ mock(fn
+ %{method: :get, url: "https://example.com/objects/invalid-content-type"} ->
+ %Tesla.Env{
+ status: 200,
+ # Not a valid AP content-type
+ headers: [{"content-type", "application/json"}],
+ body:
+ Jason.encode!(%{
+ "id" => "https://example.com/objects/invalid-content-type",
+ "type" => "Note",
+ "content" => "This has an invalid content type",
+ "actor" => "https://example.com/users/actor",
+ "attributedTo" => "https://example.com/users/actor"
+ })
+ }
+ end)
+
+ assert {:fetch, {:error, {:content_type, "application/json"}}} =
+ Fetcher.fetch_object_from_id("https://example.com/objects/invalid-content-type")
+ end
+
+ test "it accepts objects with application/ld+json and ActivityStreams profile" do
+ # Setup a mock for an object with ld+json content-type and AS profile
+ mock(fn
+ %{method: :get, url: "https://example.com/objects/valid-ld-json"} ->
+ %Tesla.Env{
+ status: 200,
+ headers: [
+ {"content-type",
+ "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}
+ ],
+ body:
+ Jason.encode!(%{
+ "id" => "https://example.com/objects/valid-ld-json",
+ "type" => "Note",
+ "content" => "This has a valid ld+json content type",
+ "actor" => "https://example.com/users/actor",
+ "attributedTo" => "https://example.com/users/actor"
+ })
+ }
+ end)
+
+ # This should pass if content-type validation works correctly
+ assert {:ok, object} =
+ Fetcher.fetch_and_contain_remote_object_from_id(
+ "https://example.com/objects/valid-ld-json"
+ )
+
+ assert object["content"] == "This has a valid ld+json content type"
+ end
+
+ test "it rejects objects with no content-type header" do
+ # Setup a mock for an object with no content-type header
+ mock(fn
+ %{method: :get, url: "https://example.com/objects/no-content-type"} ->
+ %Tesla.Env{
+ status: 200,
+ # No content-type header
+ headers: [],
+ body:
+ Jason.encode!(%{
+ "id" => "https://example.com/objects/no-content-type",
+ "type" => "Note",
+ "content" => "This has no content type header",
+ "actor" => "https://example.com/users/actor",
+ "attributedTo" => "https://example.com/users/actor"
+ })
+ }
+ end)
+
+ # We want to test that the request fails with a missing content-type error
+ # but the actual error is {:fetch, {:error, nil}} - we'll check for this format
+ result = Fetcher.fetch_object_from_id("https://example.com/objects/no-content-type")
+ assert {:fetch, {:error, nil}} = result
end
end
@@ -534,6 +607,110 @@ defmodule Pleroma.Object.FetcherTest do
end
end
+ describe "cross-domain redirect handling" do
+ setup do
+ mock(fn
+ # Cross-domain redirect with original domain in id
+ %{method: :get, url: "https://original.test/objects/123"} ->
+ %Tesla.Env{
+ status: 200,
+ url: "https://media.test/objects/123",
+ headers: [{"content-type", "application/activity+json"}],
+ body:
+ Jason.encode!(%{
+ "id" => "https://original.test/objects/123",
+ "type" => "Note",
+ "content" => "This is redirected content",
+ "actor" => "https://original.test/users/actor",
+ "attributedTo" => "https://original.test/users/actor"
+ })
+ }
+
+ # Cross-domain redirect with final domain in id
+ %{method: :get, url: "https://original.test/objects/final-domain-id"} ->
+ %Tesla.Env{
+ status: 200,
+ url: "https://media.test/objects/final-domain-id",
+ headers: [{"content-type", "application/activity+json"}],
+ body:
+ Jason.encode!(%{
+ "id" => "https://media.test/objects/final-domain-id",
+ "type" => "Note",
+ "content" => "This has final domain in id",
+ "actor" => "https://original.test/users/actor",
+ "attributedTo" => "https://original.test/users/actor"
+ })
+ }
+
+ # No redirect - same domain
+ %{method: :get, url: "https://original.test/objects/same-domain-redirect"} ->
+ %Tesla.Env{
+ status: 200,
+ url: "https://original.test/objects/different-path",
+ headers: [{"content-type", "application/activity+json"}],
+ body:
+ Jason.encode!(%{
+ "id" => "https://original.test/objects/same-domain-redirect",
+ "type" => "Note",
+ "content" => "This has a same-domain redirect",
+ "actor" => "https://original.test/users/actor",
+ "attributedTo" => "https://original.test/users/actor"
+ })
+ }
+
+ # Test case with missing url field in response (common in tests)
+ %{method: :get, url: "https://original.test/objects/missing-url"} ->
+ %Tesla.Env{
+ status: 200,
+ # No url field
+ headers: [{"content-type", "application/activity+json"}],
+ body:
+ Jason.encode!(%{
+ "id" => "https://original.test/objects/missing-url",
+ "type" => "Note",
+ "content" => "This has no URL field in response",
+ "actor" => "https://original.test/users/actor",
+ "attributedTo" => "https://original.test/users/actor"
+ })
+ }
+ end)
+
+ :ok
+ end
+
+ test "it rejects objects from cross-domain redirects with original domain in id" do
+ assert {:error, {:cross_domain_redirect, true}} =
+ Fetcher.fetch_and_contain_remote_object_from_id(
+ "https://original.test/objects/123"
+ )
+ end
+
+ test "it rejects objects from cross-domain redirects with final domain in id" do
+ assert {:error, {:cross_domain_redirect, true}} =
+ Fetcher.fetch_and_contain_remote_object_from_id(
+ "https://original.test/objects/final-domain-id"
+ )
+ end
+
+ test "it accepts objects with same-domain redirects" do
+ assert {:ok, data} =
+ Fetcher.fetch_and_contain_remote_object_from_id(
+ "https://original.test/objects/same-domain-redirect"
+ )
+
+ assert data["content"] == "This has a same-domain redirect"
+ end
+
+ test "it handles responses without URL field (common in tests)" do
+ assert {:ok, data} =
+ Fetcher.fetch_and_contain_remote_object_from_id(
+ "https://original.test/objects/missing-url"
+ )
+
+ assert data["content"] == "This has no URL field in response"
+ end
+ end
+
describe "fetch with history" do
setup do
object2 = %{
diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs
index 48d4d86eb..13e941e4d 100644
--- a/test/pleroma/object_test.exs
+++ b/test/pleroma/object_test.exs
@@ -6,12 +6,10 @@ defmodule Pleroma.ObjectTest do
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
- import ExUnit.CaptureLog
import Mox
import Pleroma.Factory
import Tesla.Mock
- alias Pleroma.Activity
alias Pleroma.Hashtag
alias Pleroma.Object
alias Pleroma.Repo
@@ -158,7 +156,7 @@ defmodule Pleroma.ObjectTest do
uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
- File.mkdir_p!(uploads_dir)
+ Pleroma.Backports.mkdir_p!(uploads_dir)
file = %Plug.Upload{
content_type: "image/jpeg",
@@ -176,8 +174,9 @@ defmodule Pleroma.ObjectTest do
filename = Path.basename(href)
- assert {:ok, files} = File.ls(uploads_dir)
- assert filename in files
+ expected_path = Path.join([uploads_dir, Pleroma.Upload.Filter.Dedupe.shard_path(filename)])
+
+ assert File.exists?(expected_path)
Object.delete(note)
@@ -185,8 +184,7 @@ defmodule Pleroma.ObjectTest do
assert Object.get_by_id(note.id).data["deleted"]
assert Object.get_by_id(attachment.id) == nil
- assert {:ok, files} = File.ls(uploads_dir)
- refute filename in files
+ refute File.exists?(expected_path)
end
test "with objects that have legacy data.url attribute" do
@@ -282,148 +280,6 @@ defmodule Pleroma.ObjectTest do
end
end
- describe "get_by_id_and_maybe_refetch" do
- setup do
- mock(fn
- %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
- %Tesla.Env{
- status: 200,
- body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
- headers: HttpRequestMock.activitypub_object_headers()
- }
-
- env ->
- apply(HttpRequestMock, :request, [env])
- end)
-
- mock_modified = fn resp ->
- mock(fn
- %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
- resp
-
- env ->
- apply(HttpRequestMock, :request, [env])
- end)
- end
-
- on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
-
- [mock_modified: mock_modified]
- end
-
- test "refetches if the time since the last refetch is greater than the interval", %{
- mock_modified: mock_modified
- } do
- %Object{} =
- object =
- Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
- fetch: true
- )
-
- Object.set_cache(object)
-
- assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
- assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
-
- mock_modified.(%Tesla.Env{
- status: 200,
- body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
- headers: HttpRequestMock.activitypub_object_headers()
- })
-
- updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
- object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
- assert updated_object == object_in_cache
- assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
- assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
- end
-
- test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
- %Object{} =
- object =
- Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
- fetch: true
- )
-
- Object.set_cache(object)
-
- assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
- assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
-
- assert capture_log(fn ->
- mock_modified.(%Tesla.Env{status: 404, body: ""})
-
- updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
- object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
- assert updated_object == object_in_cache
- assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
- assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
- end) =~
- "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
- end
-
- test "does not refetch if the time since the last refetch is greater than the interval", %{
- mock_modified: mock_modified
- } do
- %Object{} =
- object =
- Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
- fetch: true
- )
-
- Object.set_cache(object)
-
- assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
- assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
-
- mock_modified.(%Tesla.Env{
- status: 200,
- body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
- headers: HttpRequestMock.activitypub_object_headers()
- })
-
- updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
- object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
- assert updated_object == object_in_cache
- assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
- assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
- end
-
- test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
- %Object{} =
- object =
- Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
- fetch: true
- )
-
- Object.set_cache(object)
-
- assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
- assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
-
- user = insert(:user)
- activity = Activity.get_create_by_object_ap_id(object.data["id"])
- {:ok, activity} = CommonAPI.favorite(activity.id, user)
- object = Object.get_by_ap_id(activity.data["object"])
-
- assert object.data["like_count"] == 1
-
- mock_modified.(%Tesla.Env{
- status: 200,
- body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
- headers: HttpRequestMock.activitypub_object_headers()
- })
-
- updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
- object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
- assert updated_object == object_in_cache
- assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
- assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
-
- assert updated_object.data["like_count"] == 1
- end
- end
-
describe ":hashtags association" do
test "Hashtag records are created with Object record and updated on its change" do
user = insert(:user)
diff --git a/test/pleroma/otp_version_test.exs b/test/pleroma/otp_version_test.exs
deleted file mode 100644
index 21701d5a8..000000000
--- a/test/pleroma/otp_version_test.exs
+++ /dev/null
@@ -1,42 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.OTPVersionTest do
- use ExUnit.Case, async: true
-
- alias Pleroma.OTPVersion
-
- describe "check/1" do
- test "22.4" do
- assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) ==
- "22.4"
- end
-
- test "22.1" do
- assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) ==
- "22.1"
- end
-
- test "21.1" do
- assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) ==
- "21.1"
- end
-
- test "23.0" do
- assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) ==
- "23.0"
- end
-
- test "with nonexistent file" do
- assert OTPVersion.get_version_from_files([
- "test/fixtures/warnings/otp_version/non-exising",
- "test/fixtures/warnings/otp_version/22.4"
- ]) == "22.4"
- end
-
- test "empty paths" do
- assert OTPVersion.get_version_from_files([]) == nil
- end
- end
-end
diff --git a/test/pleroma/release_task_test.exs b/test/pleroma/release_task_test.exs
new file mode 100644
index 000000000..5a4293189
--- /dev/null
+++ b/test/pleroma/release_task_test.exs
@@ -0,0 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ReleaseTaskTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.ReleaseTasks
+
+ test "finding the module" do
+ task = "search.meilisearch"
+ assert Mix.Tasks.Pleroma.Search.Meilisearch == ReleaseTasks.find_module(task)
+
+ task = "user"
+ assert Mix.Tasks.Pleroma.User == ReleaseTasks.find_module(task)
+
+ refute ReleaseTasks.find_module("doesnt.exist")
+ end
+end
diff --git a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs
index 9847781f0..99522994a 100644
--- a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs
+++ b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs
@@ -3,12 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: true
import Pleroma.Factory
import Pleroma.Tests.Helpers
alias Pleroma.ConfigDB
- setup do: clear_config(Pleroma.Formatter)
setup_all do: require_migration("20200716195806_autolinker_to_linkify")
test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: migration} do
diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs
index fb330232a..034ab28a5 100644
--- a/test/pleroma/reverse_proxy_test.exs
+++ b/test/pleroma/reverse_proxy_test.exs
@@ -63,7 +63,11 @@ defmodule Pleroma.ReverseProxyTest do
|> Plug.Conn.put_req_header("user-agent", "fake/1.0")
|> ReverseProxy.call("/user-agent")
- assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()}
+ # Convert the response to a map without relying on json_response
+ body = conn.resp_body
+ assert conn.status == 200
+ response = Jason.decode!(body)
+ assert response == %{"user-agent" => Pleroma.Application.user_agent()}
end
test "closed connection", %{conn: conn} do
@@ -138,11 +142,14 @@ defmodule Pleroma.ReverseProxyTest do
test "common", %{conn: conn} do
ClientMock
|> expect(:request, fn :head, "/head", _, _, _ ->
- {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]}
+ {:ok, 200, [{"content-type", "image/png"}]}
end)
conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head")
- assert html_response(conn, 200) == ""
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
+ assert conn.resp_body == ""
end
end
@@ -249,7 +256,10 @@ defmodule Pleroma.ReverseProxyTest do
)
|> ReverseProxy.call("/headers")
- %{"headers" => headers} = json_response(conn, 200)
+ body = conn.resp_body
+ assert conn.status == 200
+ response = Jason.decode!(body)
+ headers = response["headers"]
assert headers["Accept"] == "text/html"
end
@@ -262,7 +272,10 @@ defmodule Pleroma.ReverseProxyTest do
)
|> ReverseProxy.call("/headers")
- %{"headers" => headers} = json_response(conn, 200)
+ body = conn.resp_body
+ assert conn.status == 200
+ response = Jason.decode!(body)
+ headers = response["headers"]
refute headers["Accept-Language"]
end
end
@@ -328,4 +341,94 @@ defmodule Pleroma.ReverseProxyTest do
assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
end
end
+
+ describe "content-type sanitisation" do
+ test "preserves allowed image type", %{conn: conn} do
+ ClientMock
+ |> expect(:request, fn :get, "/content", _, _, _ ->
+ {:ok, 200, [{"content-type", "image/png"}], %{url: "/content"}}
+ end)
+ |> expect(:stream_body, fn _ -> :done end)
+
+ conn = ReverseProxy.call(conn, "/content")
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
+ end
+
+ test "preserves allowed video type", %{conn: conn} do
+ ClientMock
+ |> expect(:request, fn :get, "/content", _, _, _ ->
+ {:ok, 200, [{"content-type", "video/mp4"}], %{url: "/content"}}
+ end)
+ |> expect(:stream_body, fn _ -> :done end)
+
+ conn = ReverseProxy.call(conn, "/content")
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["video/mp4"]
+ end
+
+ test "sanitizes ActivityPub content type", %{conn: conn} do
+ ClientMock
+ |> expect(:request, fn :get, "/content", _, _, _ ->
+ {:ok, 200, [{"content-type", "application/activity+json"}], %{url: "/content"}}
+ end)
+ |> expect(:stream_body, fn _ -> :done end)
+
+ conn = ReverseProxy.call(conn, "/content")
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
+ end
+
+ test "sanitizes LD-JSON content type", %{conn: conn} do
+ ClientMock
+ |> expect(:request, fn :get, "/content", _, _, _ ->
+ {:ok, 200, [{"content-type", "application/ld+json"}], %{url: "/content"}}
+ end)
+ |> expect(:stream_body, fn _ -> :done end)
+
+ conn = ReverseProxy.call(conn, "/content")
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
+ end
+ end
+
+ # Hackey is used for Reverse Proxy when Hackney or Finch is the Tesla Adapter
+ # Gun is able to proxy through Tesla, so it does not need testing as the
+ # test cases in the Pleroma.HTTPTest module are sufficient
+ describe "Hackney URL encoding:" do
+ setup do
+ ClientMock
+ |> expect(:request, fn :get,
+ "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz",
+ _headers,
+ _body,
+ _opts ->
+ {:ok, 200, [{"content-type", "image/png"}], "It works!"}
+ end)
+ |> stub(:stream_body, fn _ -> :done end)
+ |> stub(:close, fn _ -> :ok end)
+
+ :ok
+ end
+
+ test "properly encodes URLs with spaces", %{conn: conn} do
+ url_with_space = "https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz"
+
+ result = ReverseProxy.call(conn, url_with_space)
+
+ assert result.status == 200
+ end
+
+ test "properly encoded URL should not be altered", %{conn: conn} do
+ properly_encoded_url = "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"
+
+ result = ReverseProxy.call(conn, properly_encoded_url)
+
+ assert result.status == 200
+ end
+ end
end
diff --git a/test/pleroma/safe_zip_test.exs b/test/pleroma/safe_zip_test.exs
new file mode 100644
index 000000000..f07b25675
--- /dev/null
+++ b/test/pleroma/safe_zip_test.exs
@@ -0,0 +1,496 @@
+defmodule Pleroma.SafeZipTest do
+ # Not making this async because it creates and deletes files
+ use ExUnit.Case
+
+ alias Pleroma.SafeZip
+
+ @fixtures_dir "test/fixtures"
+ @tmp_dir "test/zip_tmp"
+
+ setup do
+ # Ensure tmp directory exists
+ Pleroma.Backports.mkdir_p!(@tmp_dir)
+
+ on_exit(fn ->
+ # Clean up any files created during tests
+ File.rm_rf!(@tmp_dir)
+ Pleroma.Backports.mkdir_p!(@tmp_dir)
+ end)
+
+ :ok
+ end
+
+ describe "list_dir_file/1" do
+ test "lists files in a valid zip" do
+ {:ok, files} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "emojis.zip"))
+ assert is_list(files)
+ assert length(files) > 0
+ end
+
+ test "returns an empty list for empty zip" do
+ {:ok, files} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "empty.zip"))
+ assert files == []
+ end
+
+ test "returns error for non-existent file" do
+ assert {:error, _} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "nonexistent.zip"))
+ end
+
+ test "only lists regular files, not directories" do
+ # Create a zip with both files and directories
+ zip_path = create_zip_with_directory()
+
+ # List files with SafeZip
+ {:ok, files} = SafeZip.list_dir_file(zip_path)
+
+ # Verify only regular files are listed, not directories
+ assert "file_in_dir/test_file.txt" in files
+ assert "root_file.txt" in files
+
+ # Directory entries should not be included in the list
+ refute "file_in_dir/" in files
+ end
+ end
+
+ describe "contains_all_data?/2" do
+ test "returns true when all files are in the archive" do
+ # For this test, we'll create our own zip file with known content
+ # to ensure we can test the contains_all_data? function properly
+ zip_path = create_zip_with_directory()
+ archive_data = File.read!(zip_path)
+
+ # Check if the archive contains the root file
+ # Note: The function expects charlists (Erlang strings) in the MapSet
+ assert SafeZip.contains_all_data?(archive_data, MapSet.new([~c"root_file.txt"]))
+ end
+
+ test "returns false when files are missing" do
+ archive_path = Path.join(@fixtures_dir, "emojis.zip")
+ archive_data = File.read!(archive_path)
+
+ # Create a MapSet with non-existent files
+ fset = MapSet.new([~c"nonexistent.txt"])
+
+ refute SafeZip.contains_all_data?(archive_data, fset)
+ end
+
+ test "returns false for invalid archive data" do
+ refute SafeZip.contains_all_data?("invalid data", MapSet.new([~c"file.txt"]))
+ end
+
+ test "only checks for regular files, not directories" do
+ # Create a zip with both files and directories
+ zip_path = create_zip_with_directory()
+ archive_data = File.read!(zip_path)
+
+ # Check if the archive contains a directory (should return false)
+ refute SafeZip.contains_all_data?(archive_data, MapSet.new([~c"file_in_dir/"]))
+
+ # For this test, we'll manually check if the file exists in the archive
+ # by extracting it and verifying it exists
+ extract_dir = Path.join(@tmp_dir, "extract_check")
+ Pleroma.Backports.mkdir_p!(extract_dir)
+ {:ok, files} = SafeZip.unzip_file(zip_path, extract_dir)
+
+ # Verify the root file was extracted
+ assert Enum.any?(files, fn file ->
+ Path.basename(file) == "root_file.txt"
+ end)
+
+ # Verify the file exists on disk
+ assert File.exists?(Path.join(extract_dir, "root_file.txt"))
+ end
+ end
+
+ describe "zip/4" do
+ test "creates a zip file on disk" do
+ # Create a test file
+ test_file_path = Path.join(@tmp_dir, "test_file.txt")
+ File.write!(test_file_path, "test content")
+
+ # Create a zip file
+ zip_path = Path.join(@tmp_dir, "test.zip")
+ assert {:ok, ^zip_path} = SafeZip.zip(zip_path, ["test_file.txt"], @tmp_dir, false)
+
+ # Verify the zip file exists
+ assert File.exists?(zip_path)
+ end
+
+ test "creates a zip file in memory" do
+ # Create a test file
+ test_file_path = Path.join(@tmp_dir, "test_file.txt")
+ File.write!(test_file_path, "test content")
+
+ # Create a zip file in memory
+ zip_name = Path.join(@tmp_dir, "test.zip")
+
+ assert {:ok, {^zip_name, zip_data}} =
+ SafeZip.zip(zip_name, ["test_file.txt"], @tmp_dir, true)
+
+ # Verify the zip data is binary
+ assert is_binary(zip_data)
+ end
+
+ test "returns error for unsafe paths" do
+ # Try to zip a file with path traversal
+ assert {:error, _} =
+ SafeZip.zip(
+ Path.join(@tmp_dir, "test.zip"),
+ ["../fixtures/test.txt"],
+ @tmp_dir,
+ false
+ )
+ end
+
+ test "can create zip with directories" do
+ # Create a directory structure
+ dir_path = Path.join(@tmp_dir, "test_dir")
+ Pleroma.Backports.mkdir_p!(dir_path)
+
+ file_in_dir_path = Path.join(dir_path, "file_in_dir.txt")
+ File.write!(file_in_dir_path, "file in directory")
+
+ # Create a zip file
+ zip_path = Path.join(@tmp_dir, "dir_test.zip")
+
+ assert {:ok, ^zip_path} =
+ SafeZip.zip(
+ zip_path,
+ ["test_dir/file_in_dir.txt"],
+ @tmp_dir,
+ false
+ )
+
+ # Verify the zip file exists
+ assert File.exists?(zip_path)
+
+ # Extract and verify the directory structure is preserved
+ extract_dir = Path.join(@tmp_dir, "extract")
+ {:ok, files} = SafeZip.unzip_file(zip_path, extract_dir)
+
+ # Check if the file path is in the list, accounting for possible full paths
+ assert Enum.any?(files, fn file ->
+ String.ends_with?(file, "file_in_dir.txt")
+ end)
+
+ # Verify the file exists in the expected location
+ assert File.exists?(Path.join([extract_dir, "test_dir", "file_in_dir.txt"]))
+ end
+ end
+
+ describe "unzip_file/3" do
+ test "extracts files from a zip archive" do
+ archive_path = Path.join(@fixtures_dir, "emojis.zip")
+
+ # Extract the archive
+ assert {:ok, files} = SafeZip.unzip_file(archive_path, @tmp_dir)
+
+ # Verify files were extracted
+ assert is_list(files)
+ assert length(files) > 0
+
+ # Verify at least one file exists
+ first_file = List.first(files)
+
+ # Simply check that the file exists in the tmp directory
+ assert File.exists?(first_file)
+ end
+
+ test "extracts specific files from a zip archive" do
+ archive_path = Path.join(@fixtures_dir, "emojis.zip")
+
+ # Get list of files in the archive
+ {:ok, all_files} = SafeZip.list_dir_file(archive_path)
+ file_to_extract = List.first(all_files)
+
+ # Extract only one file
+ assert {:ok, [extracted_file]} =
+ SafeZip.unzip_file(archive_path, @tmp_dir, [file_to_extract])
+
+ # Verify only the specified file was extracted
+ assert Path.basename(extracted_file) == Path.basename(file_to_extract)
+
+ # Check that the file exists in the tmp directory
+ assert File.exists?(Path.join(@tmp_dir, Path.basename(file_to_extract)))
+ end
+
+ test "returns error for invalid zip file" do
+ invalid_path = Path.join(@tmp_dir, "invalid.zip")
+ File.write!(invalid_path, "not a zip file")
+
+ assert {:error, _} = SafeZip.unzip_file(invalid_path, @tmp_dir)
+ end
+
+ test "creates directories when extracting files in subdirectories" do
+ # Create a zip with files in subdirectories
+ zip_path = create_zip_with_directory()
+
+ # Extract the archive
+ assert {:ok, files} = SafeZip.unzip_file(zip_path, @tmp_dir)
+
+ # Verify files were extracted - handle both relative and absolute paths
+ assert Enum.any?(files, fn file ->
+ Path.basename(file) == "test_file.txt" &&
+ String.contains?(file, "file_in_dir")
+ end)
+
+ assert Enum.any?(files, fn file ->
+ Path.basename(file) == "root_file.txt"
+ end)
+
+ # Verify directory was created
+ dir_path = Path.join(@tmp_dir, "file_in_dir")
+ assert File.exists?(dir_path)
+ assert File.dir?(dir_path)
+
+ # Verify file in directory was extracted
+ file_path = Path.join(dir_path, "test_file.txt")
+ assert File.exists?(file_path)
+ end
+ end
+
+ describe "unzip_data/3" do
+ test "extracts files from zip data" do
+ archive_path = Path.join(@fixtures_dir, "emojis.zip")
+ archive_data = File.read!(archive_path)
+
+ # Extract the archive from data
+ assert {:ok, files} = SafeZip.unzip_data(archive_data, @tmp_dir)
+
+ # Verify files were extracted
+ assert is_list(files)
+ assert length(files) > 0
+
+ # Verify at least one file exists
+ first_file = List.first(files)
+
+ # Simply check that the file exists in the tmp directory
+ assert File.exists?(first_file)
+ end
+
+ test "extracts specific files from zip data" do
+ archive_path = Path.join(@fixtures_dir, "emojis.zip")
+ archive_data = File.read!(archive_path)
+
+ # Get list of files in the archive
+ {:ok, all_files} = SafeZip.list_dir_file(archive_path)
+ file_to_extract = List.first(all_files)
+
+ # Extract only one file
+ assert {:ok, extracted_files} =
+ SafeZip.unzip_data(archive_data, @tmp_dir, [file_to_extract])
+
+ # Verify only the specified file was extracted
+ assert Enum.any?(extracted_files, fn path ->
+ Path.basename(path) == Path.basename(file_to_extract)
+ end)
+
+ # Simply check that the file exists in the tmp directory
+ assert File.exists?(Path.join(@tmp_dir, Path.basename(file_to_extract)))
+ end
+
+ test "returns error for invalid zip data" do
+ assert {:error, _} = SafeZip.unzip_data("not a zip file", @tmp_dir)
+ end
+
+ test "creates directories when extracting files in subdirectories from data" do
+ # Create a zip with files in subdirectories
+ zip_path = create_zip_with_directory()
+ archive_data = File.read!(zip_path)
+
+ # Extract the archive from data
+ assert {:ok, files} = SafeZip.unzip_data(archive_data, @tmp_dir)
+
+ # Verify files were extracted - handle both relative and absolute paths
+ assert Enum.any?(files, fn file ->
+ Path.basename(file) == "test_file.txt" &&
+ String.contains?(file, "file_in_dir")
+ end)
+
+ assert Enum.any?(files, fn file ->
+ Path.basename(file) == "root_file.txt"
+ end)
+
+ # Verify directory was created
+ dir_path = Path.join(@tmp_dir, "file_in_dir")
+ assert File.exists?(dir_path)
+ assert File.dir?(dir_path)
+
+ # Verify file in directory was extracted
+ file_path = Path.join(dir_path, "test_file.txt")
+ assert File.exists?(file_path)
+ end
+ end
+
+ # Security tests
+ describe "security checks" do
+ test "prevents path traversal in zip extraction" do
+ # Create a malicious zip file with path traversal
+ malicious_zip_path = create_malicious_zip_with_path_traversal()
+
+ # Try to extract it with SafeZip
+ assert {:error, _} = SafeZip.unzip_file(malicious_zip_path, @tmp_dir)
+
+ # Verify the file was not extracted outside the target directory
+ refute File.exists?(Path.join(Path.dirname(@tmp_dir), "traversal_attempt.txt"))
+ end
+
+ test "prevents directory traversal in zip listing" do
+ # Create a malicious zip file with path traversal
+ malicious_zip_path = create_malicious_zip_with_path_traversal()
+
+ # Try to list files with SafeZip
+ assert {:error, _} = SafeZip.list_dir_file(malicious_zip_path)
+ end
+
+ test "prevents path traversal in zip data extraction" do
+ # Create a malicious zip file with path traversal
+ malicious_zip_path = create_malicious_zip_with_path_traversal()
+ malicious_data = File.read!(malicious_zip_path)
+
+ # Try to extract it with SafeZip
+ assert {:error, _} = SafeZip.unzip_data(malicious_data, @tmp_dir)
+
+ # Verify the file was not extracted outside the target directory
+ refute File.exists?(Path.join(Path.dirname(@tmp_dir), "traversal_attempt.txt"))
+ end
+
+ test "handles zip bomb attempts" do
+ # Create a zip bomb (a zip with many files or large files)
+ zip_bomb_path = create_zip_bomb()
+
+ # The SafeZip module should handle this gracefully
+ # Either by successfully extracting it (if it's not too large)
+ # or by returning an error (if it detects a potential zip bomb)
+ result = SafeZip.unzip_file(zip_bomb_path, @tmp_dir)
+
+ case result do
+ {:ok, _} ->
+ # If it successfully extracts, make sure it didn't fill up the disk
+ # This is a simple check to ensure the extraction was controlled
+ assert File.exists?(@tmp_dir)
+
+ {:error, _} ->
+ # If it returns an error, that's also acceptable
+ # The important thing is that it doesn't crash or hang
+ assert true
+ end
+ end
+
+ test "handles deeply nested directory structures" do
+ # Create a zip with deeply nested directories
+ deep_nest_path = create_deeply_nested_zip()
+
+ # The SafeZip module should handle this gracefully
+ result = SafeZip.unzip_file(deep_nest_path, @tmp_dir)
+
+ case result do
+ {:ok, files} ->
+ # If it successfully extracts, verify the files were extracted
+ assert is_list(files)
+ assert length(files) > 0
+
+ {:error, _} ->
+ # If it returns an error, that's also acceptable
+ # The important thing is that it doesn't crash or hang
+ assert true
+ end
+ end
+ end
+
+ # Helper functions to create test fixtures
+
+ # Creates a zip file with a path traversal attempt
+ defp create_malicious_zip_with_path_traversal do
+ malicious_zip_path = Path.join(@tmp_dir, "path_traversal.zip")
+
+ # Create a file to include in the zip
+ test_file_path = Path.join(@tmp_dir, "test_file.txt")
+ File.write!(test_file_path, "malicious content")
+
+ # Use Erlang's zip module directly to create a zip with path traversal
+ {:ok, charlist_path} =
+ :zip.create(
+ String.to_charlist(malicious_zip_path),
+ [{String.to_charlist("../traversal_attempt.txt"), File.read!(test_file_path)}]
+ )
+
+ to_string(charlist_path)
+ end
+
+ # Creates a zip file with directory entries
+ defp create_zip_with_directory do
+ zip_path = Path.join(@tmp_dir, "with_directory.zip")
+
+ # Create files to include in the zip
+ root_file_path = Path.join(@tmp_dir, "root_file.txt")
+ File.write!(root_file_path, "root file content")
+
+ # Create a directory and a file in it
+ dir_path = Path.join(@tmp_dir, "file_in_dir")
+ Pleroma.Backports.mkdir_p!(dir_path)
+
+ file_in_dir_path = Path.join(dir_path, "test_file.txt")
+ File.write!(file_in_dir_path, "file in directory content")
+
+ # Use Erlang's zip module to create a zip with directory structure
+ {:ok, charlist_path} =
+ :zip.create(
+ String.to_charlist(zip_path),
+ [
+ {String.to_charlist("root_file.txt"), File.read!(root_file_path)},
+ {String.to_charlist("file_in_dir/test_file.txt"), File.read!(file_in_dir_path)}
+ ]
+ )
+
+ to_string(charlist_path)
+ end
+
+ # Creates a zip bomb (a zip with many small files)
+ defp create_zip_bomb do
+ zip_path = Path.join(@tmp_dir, "zip_bomb.zip")
+
+ # Create a small file to duplicate many times
+ small_file_path = Path.join(@tmp_dir, "small_file.txt")
+ File.write!(small_file_path, String.duplicate("A", 100))
+
+ # Create a list of many files to include in the zip
+ file_entries =
+ for i <- 1..100 do
+ {String.to_charlist("file_#{i}.txt"), File.read!(small_file_path)}
+ end
+
+ # Use Erlang's zip module to create a zip with many files
+ {:ok, charlist_path} =
+ :zip.create(
+ String.to_charlist(zip_path),
+ file_entries
+ )
+
+ to_string(charlist_path)
+ end
+
+ # Creates a zip with deeply nested directories
+ defp create_deeply_nested_zip do
+ zip_path = Path.join(@tmp_dir, "deep_nest.zip")
+
+ # Create a file to include in the zip
+ file_content = "test content"
+
+ # Create a list of deeply nested files
+ file_entries =
+ for i <- 1..10 do
+ nested_path = Enum.reduce(1..i, "nested", fn j, acc -> "#{acc}/level_#{j}" end)
+ {String.to_charlist("#{nested_path}/file.txt"), file_content}
+ end
+
+ # Use Erlang's zip module to create a zip with deeply nested directories
+ {:ok, charlist_path} =
+ :zip.create(
+ String.to_charlist(zip_path),
+ file_entries
+ )
+
+ to_string(charlist_path)
+ end
+end
diff --git a/test/pleroma/search/qdrant_search_test.exs b/test/pleroma/search/qdrant_search_test.exs
index 47a77a391..daf8eeb69 100644
--- a/test/pleroma/search/qdrant_search_test.exs
+++ b/test/pleroma/search/qdrant_search_test.exs
@@ -51,7 +51,7 @@ defmodule Pleroma.Search.QdrantSearchTest do
})
Config
- |> expect(:get, 3, fn
+ |> expect(:get, 4, fn
[Pleroma.Search, :module], nil ->
QdrantSearch
@@ -93,7 +93,7 @@ defmodule Pleroma.Search.QdrantSearchTest do
})
Config
- |> expect(:get, 3, fn
+ |> expect(:get, 4, fn
[Pleroma.Search, :module], nil ->
QdrantSearch
@@ -158,7 +158,7 @@ defmodule Pleroma.Search.QdrantSearchTest do
end)
Config
- |> expect(:get, 6, fn
+ |> expect(:get, 7, fn
[Pleroma.Search, :module], nil ->
QdrantSearch
diff --git a/test/pleroma/upload/filter/analyze_metadata_test.exs b/test/pleroma/upload/filter/analyze_metadata_test.exs
index e4ac673b2..6e1f2afaf 100644
--- a/test/pleroma/upload/filter/analyze_metadata_test.exs
+++ b/test/pleroma/upload/filter/analyze_metadata_test.exs
@@ -34,6 +34,20 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadataTest do
assert meta.blurhash == "eXJi-E:SwCEm5rCmn$+YWYn+15K#5A$xxCi{SiV]s*W:Efa#s.jE-T"
end
+ test "it gets dimensions for grayscale images" do
+ upload = %Pleroma.Upload{
+ name: "break_analyze.png",
+ content_type: "image/png",
+ path: Path.absname("test/fixtures/break_analyze.png"),
+ tempfile: Path.absname("test/fixtures/break_analyze.png")
+ }
+
+ {:ok, :filtered, meta} = AnalyzeMetadata.filter(upload)
+
+ assert %{width: 1410, height: 2048} = meta
+ assert is_nil(meta.blurhash)
+ end
+
test "adds the dimensions for videos" do
upload = %Pleroma.Upload{
name: "coolvideo.mp4",
diff --git a/test/pleroma/upload/filter/anonymize_filename_test.exs b/test/pleroma/upload/filter/anonymize_filename_test.exs
index 9b94b91c3..5dae62003 100644
--- a/test/pleroma/upload/filter/anonymize_filename_test.exs
+++ b/test/pleroma/upload/filter/anonymize_filename_test.exs
@@ -3,8 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: true
+ import Mox
+ alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
alias Pleroma.Upload
setup do
@@ -19,21 +21,26 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
%{upload_file: upload_file}
end
- setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
-
test "it replaces filename on pre-defined text", %{upload_file: upload_file} do
- clear_config([Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
+ ConfigMock
+ |> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> "custom-file.png" end)
+
{:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
assert name == "custom-file.png"
end
test "it replaces filename on pre-defined text expression", %{upload_file: upload_file} do
- clear_config([Upload.Filter.AnonymizeFilename, :text], "custom-file.{extension}")
+ ConfigMock
+ |> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> "custom-file.{extension}" end)
+
{:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
assert name == "custom-file.jpg"
end
test "it replaces filename on random text", %{upload_file: upload_file} do
+ ConfigMock
+ |> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> nil end)
+
{:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
assert <<_::bytes-size(14)>> <> ".jpg" = name
refute name == "an… image.jpg"
diff --git a/test/pleroma/upload/filter/dedupe_test.exs b/test/pleroma/upload/filter/dedupe_test.exs
index 29c181509..4dc28b998 100644
--- a/test/pleroma/upload/filter/dedupe_test.exs
+++ b/test/pleroma/upload/filter/dedupe_test.exs
@@ -10,6 +10,10 @@ defmodule Pleroma.Upload.Filter.DedupeTest do
@shasum "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781"
+ test "generates a shard path for a shasum" do
+ assert "e3/03/97/" <> _path = Dedupe.shard_path(@shasum)
+ end
+
test "adds shasum" do
File.cp!(
"test/fixtures/image.jpg",
@@ -23,10 +27,12 @@ defmodule Pleroma.Upload.Filter.DedupeTest do
tempfile: Path.absname("test/fixtures/image_tmp.jpg")
}
+ expected_path = Dedupe.shard_path(@shasum <> ".jpg")
+
assert {
:ok,
:filtered,
- %Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"}
+ %Pleroma.Upload{id: @shasum, path: ^expected_path}
} = Dedupe.filter(upload)
end
end
diff --git a/test/pleroma/upload/filter/mogrifun_test.exs b/test/pleroma/upload/filter/mogrifun_test.exs
index bf9b65589..77a9c1666 100644
--- a/test/pleroma/upload/filter/mogrifun_test.exs
+++ b/test/pleroma/upload/filter/mogrifun_test.exs
@@ -3,9 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.MogrifunTest do
- use Pleroma.DataCase
- import Mock
+ use Pleroma.DataCase, async: true
+ import Mox
+ alias Pleroma.MogrifyMock
alias Pleroma.Upload
alias Pleroma.Upload.Filter
@@ -22,23 +23,12 @@ defmodule Pleroma.Upload.Filter.MogrifunTest do
tempfile: Path.absname("test/fixtures/image_tmp.jpg")
}
- task =
- Task.async(fn ->
- assert_receive {:apply_filter, {}}, 4_000
- end)
+ MogrifyMock
+ |> stub(:open, fn _file -> %{} end)
+ |> stub(:custom, fn _image, _action -> %{} end)
+ |> stub(:custom, fn _image, _action, _options -> %{} end)
+ |> stub(:save, fn _image, [in_place: true] -> :ok end)
- with_mocks([
- {Mogrify, [],
- [
- open: fn _f -> %Mogrify.Image{} end,
- custom: fn _m, _a -> send(task.pid, {:apply_filter, {}}) end,
- custom: fn _m, _a, _o -> send(task.pid, {:apply_filter, {}}) end,
- save: fn _f, _o -> :ok end
- ]}
- ]) do
- assert Filter.Mogrifun.filter(upload) == {:ok, :filtered}
- end
-
- Task.await(task)
+ assert Filter.Mogrifun.filter(upload) == {:ok, :filtered}
end
end
diff --git a/test/pleroma/upload/filter/mogrify_test.exs b/test/pleroma/upload/filter/mogrify_test.exs
index 208da57ca..f8ed6e8dd 100644
--- a/test/pleroma/upload/filter/mogrify_test.exs
+++ b/test/pleroma/upload/filter/mogrify_test.exs
@@ -3,13 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.MogrifyTest do
- use Pleroma.DataCase
- import Mock
+ use Pleroma.DataCase, async: true
+ import Mox
+ alias Pleroma.MogrifyMock
+ alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
alias Pleroma.Upload.Filter
+ setup :verify_on_exit!
+
test "apply mogrify filter" do
- clear_config(Filter.Mogrify, args: [{"tint", "40"}])
+ ConfigMock
+ |> stub(:get!, fn [Filter.Mogrify, :args] -> [{"tint", "40"}] end)
File.cp!(
"test/fixtures/image.jpg",
@@ -23,19 +28,11 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do
tempfile: Path.absname("test/fixtures/image_tmp.jpg")
}
- task =
- Task.async(fn ->
- assert_receive {:apply_filter, {_, "tint", "40"}}, 4_000
- end)
+ MogrifyMock
+ |> expect(:open, fn _file -> %{} end)
+ |> expect(:custom, fn _image, "tint", "40" -> %{} end)
+ |> expect(:save, fn _image, [in_place: true] -> :ok end)
- with_mock Mogrify,
- open: fn _f -> %Mogrify.Image{} end,
- custom: fn _m, _a -> :ok end,
- custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end,
- save: fn _f, _o -> :ok end do
- assert Filter.Mogrify.filter(upload) == {:ok, :filtered}
- end
-
- Task.await(task)
+ assert Filter.Mogrify.filter(upload) == {:ok, :filtered}
end
end
diff --git a/test/pleroma/upload/filter_test.exs b/test/pleroma/upload/filter_test.exs
index 706fc9ac7..a369a723a 100644
--- a/test/pleroma/upload/filter_test.exs
+++ b/test/pleroma/upload/filter_test.exs
@@ -5,12 +5,13 @@
defmodule Pleroma.Upload.FilterTest do
use Pleroma.DataCase
+ import Mox
+ alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
alias Pleroma.Upload.Filter
- setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
-
test "applies filters" do
- clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
+ ConfigMock
+ |> stub(:get, fn [Pleroma.Upload.Filter.AnonymizeFilename, :text] -> "custom-file.png" end)
File.cp!(
"test/fixtures/image.jpg",
diff --git a/test/pleroma/upload_test.exs b/test/pleroma/upload_test.exs
index facb634c3..5fd62fa43 100644
--- a/test/pleroma/upload_test.exs
+++ b/test/pleroma/upload_test.exs
@@ -149,6 +149,9 @@ defmodule Pleroma.UploadTest do
test "copies the file to the configured folder with deduping" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
+ expected_filename = "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg"
+
+ expected_path = Pleroma.Upload.Filter.Dedupe.shard_path(expected_filename)
file = %Plug.Upload{
content_type: "image/jpeg",
@@ -159,8 +162,7 @@ defmodule Pleroma.UploadTest do
{:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe])
assert List.first(data["url"])["href"] ==
- Pleroma.Upload.base_url() <>
- "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg"
+ Path.join([Pleroma.Upload.base_url(), expected_path])
end
test "copies the file to the configured folder without deduping" do
diff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs
index 24fe09f7e..f4b92adf8 100644
--- a/test/pleroma/user/backup_test.exs
+++ b/test/pleroma/user/backup_test.exs
@@ -185,13 +185,13 @@ defmodule Pleroma.User.BackupTest do
%{"@language" => "und"}
],
"bookmarks" => "bookmarks.json",
- "followers" => "http://cofe.io/users/cofe/followers",
- "following" => "http://cofe.io/users/cofe/following",
+ "followers" => "followers.json",
+ "following" => "following.json",
"id" => "http://cofe.io/users/cofe",
"inbox" => "http://cofe.io/users/cofe/inbox",
"likes" => "likes.json",
"name" => "Cofe",
- "outbox" => "http://cofe.io/users/cofe/outbox",
+ "outbox" => "outbox.json",
"preferredUsername" => "cofe",
"publicKey" => %{
"id" => "http://cofe.io/users/cofe#main-key",
diff --git a/test/pleroma/user_relationship_test.exs b/test/pleroma/user_relationship_test.exs
index 7d205a746..5b43cb2b6 100644
--- a/test/pleroma/user_relationship_test.exs
+++ b/test/pleroma/user_relationship_test.exs
@@ -3,11 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserRelationshipTest do
+ alias Pleroma.DateTimeMock
alias Pleroma.UserRelationship
- use Pleroma.DataCase, async: false
+ use Pleroma.DataCase, async: true
- import Mock
+ import Mox
import Pleroma.Factory
describe "*_exists?/2" do
@@ -52,6 +53,9 @@ defmodule Pleroma.UserRelationshipTest do
end
test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do
+ DateTimeMock
+ |> stub_with(Pleroma.DateTime.Impl)
+
for relationship_type <- [
:block,
:mute,
@@ -80,13 +84,15 @@ defmodule Pleroma.UserRelationshipTest do
end
test "if record already exists, returns it", %{users: [user1, user2]} do
- user_block =
- with_mock NaiveDateTime, [:passthrough], utc_now: fn -> ~N[2017-03-17 17:09:58] end do
- {:ok, %{inserted_at: ~N[2017-03-17 17:09:58]}} =
- UserRelationship.create_block(user1, user2)
- end
+ fixed_datetime = ~N[2017-03-17 17:09:58]
- assert user_block == UserRelationship.create_block(user1, user2)
+ Pleroma.DateTimeMock
+ |> expect(:utc_now, 2, fn -> fixed_datetime end)
+
+ {:ok, %{inserted_at: ^fixed_datetime}} = UserRelationship.create_block(user1, user2)
+
+ # Test the idempotency without caring about the exact time
+ assert {:ok, _} = UserRelationship.create_block(user1, user2)
end
end
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index 06afc0709..0b4dc9197 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -20,7 +20,7 @@ defmodule Pleroma.UserTest do
import Swoosh.TestAssertions
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
@@ -2405,8 +2405,8 @@ defmodule Pleroma.UserTest do
other_user =
insert(:user,
local: false,
- follower_address: "http://localhost:4001/users/masto_closed/followers",
- following_address: "http://localhost:4001/users/masto_closed/following"
+ follower_address: "https://remote.org/users/masto_closed/followers",
+ following_address: "https://remote.org/users/masto_closed/following"
)
assert other_user.following_count == 0
@@ -2426,8 +2426,8 @@ defmodule Pleroma.UserTest do
other_user =
insert(:user,
local: false,
- follower_address: "http://localhost:4001/users/masto_closed/followers",
- following_address: "http://localhost:4001/users/masto_closed/following"
+ follower_address: "https://remote.org/users/masto_closed/followers",
+ following_address: "https://remote.org/users/masto_closed/following"
)
assert other_user.following_count == 0
@@ -2447,8 +2447,8 @@ defmodule Pleroma.UserTest do
other_user =
insert(:user,
local: false,
- follower_address: "http://localhost:4001/users/masto_closed/followers",
- following_address: "http://localhost:4001/users/masto_closed/following"
+ follower_address: "https://remote.org/users/masto_closed/followers",
+ following_address: "https://remote.org/users/masto_closed/following"
)
assert other_user.following_count == 0
@@ -2669,8 +2669,12 @@ defmodule Pleroma.UserTest do
assert {:ok, user} = User.update_last_active_at(user)
- assert user.last_active_at >= test_started_at
- assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+ assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq]
+
+ assert NaiveDateTime.compare(
+ user.last_active_at,
+ NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+ ) in [:lt, :eq]
last_active_at =
NaiveDateTime.utc_now()
@@ -2682,10 +2686,15 @@ defmodule Pleroma.UserTest do
|> cast(%{last_active_at: last_active_at}, [:last_active_at])
|> User.update_and_set_cache()
- assert user.last_active_at == last_active_at
+ assert NaiveDateTime.compare(user.last_active_at, last_active_at) == :eq
+
assert {:ok, user} = User.update_last_active_at(user)
- assert user.last_active_at >= test_started_at
- assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+ assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq]
+
+ assert NaiveDateTime.compare(
+ user.last_active_at,
+ NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+ ) in [:lt, :eq]
end
test "active_user_count/1" do
@@ -2783,6 +2792,15 @@ defmodule Pleroma.UserTest do
assert user_updated.also_known_as |> length() == 1
assert user2.ap_id in user_updated.also_known_as
end
+
+ test "should tolerate non-http(s) aliases" do
+ user =
+ insert(:user, %{
+ also_known_as: ["at://did:plc:xgvzy7ni6ig6ievcbls5jaxe"]
+ })
+
+ assert "at://did:plc:xgvzy7ni6ig6ievcbls5jaxe" in user.also_known_as
+ end
end
describe "alias_users/1" do
@@ -2919,4 +2937,74 @@ defmodule Pleroma.UserTest do
assert [%{"verified_at" => ^verified_at}] = user.fields
end
+
+ describe "follow_hashtag/2" do
+ test "should follow a hashtag" do
+ user = insert(:user)
+ hashtag = insert(:hashtag)
+
+ assert {:ok, _} = user |> User.follow_hashtag(hashtag)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert user.followed_hashtags |> Enum.count() == 1
+ assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
+ end
+
+ test "should not follow a hashtag twice" do
+ user = insert(:user)
+ hashtag = insert(:hashtag)
+
+ assert {:ok, _} = user |> User.follow_hashtag(hashtag)
+
+ assert {:ok, _} = user |> User.follow_hashtag(hashtag)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert user.followed_hashtags |> Enum.count() == 1
+ assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
+ end
+
+ test "can follow multiple hashtags" do
+ user = insert(:user)
+ hashtag = insert(:hashtag)
+ other_hashtag = insert(:hashtag)
+
+ assert {:ok, _} = user |> User.follow_hashtag(hashtag)
+ assert {:ok, _} = user |> User.follow_hashtag(other_hashtag)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert user.followed_hashtags |> Enum.count() == 2
+ assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
+ assert other_hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
+ end
+ end
+
+ describe "unfollow_hashtag/2" do
+ test "should unfollow a hashtag" do
+ user = insert(:user)
+ hashtag = insert(:hashtag)
+
+ assert {:ok, _} = user |> User.follow_hashtag(hashtag)
+ assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert user.followed_hashtags |> Enum.count() == 0
+ end
+
+ test "should not error when trying to unfollow a hashtag twice" do
+ user = insert(:user)
+ hashtag = insert(:hashtag)
+
+ assert {:ok, _} = user |> User.follow_hashtag(hashtag)
+ assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
+ assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
+
+ user = User.get_cached_by_ap_id(user.ap_id)
+
+ assert user.followed_hashtags |> Enum.count() == 0
+ end
+ end
end
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
index 3bd589f49..2bdd58dba 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
alias Pleroma.Activity
alias Pleroma.Delivery
- alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
@@ -26,7 +25,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
require Pleroma.Constants
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
@@ -601,23 +600,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
- 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"]
- Instances.set_consistently_unreachable(sender_url)
- refute Instances.reachable?(sender_url)
-
- conn =
- conn
- |> assign(:valid_signature, true)
- |> put_req_header("content-type", "application/activity+json")
- |> post("/inbox", data)
-
- assert "ok" == json_response(conn, 200)
- assert Instances.reachable?(sender_url)
- end
-
test "accept follow activity", %{conn: conn} do
clear_config([:instance, :federating], true)
relay = Relay.get_actor()
@@ -941,23 +923,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
- test "it rejects an invalid incoming activity", %{conn: conn, data: data} do
- user = insert(:user, is_active: false)
-
- data =
- data
- |> Map.put("bcc", [user.ap_id])
- |> Kernel.put_in(["object", "bcc"], [user.ap_id])
-
- conn =
- conn
- |> assign(:valid_signature, true)
- |> put_req_header("content-type", "application/activity+json")
- |> post("/users/#{user.nickname}/inbox", data)
-
- assert "Invalid request." == json_response(conn, 400)
- end
-
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
user = insert(:user)
@@ -1108,24 +1073,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert response(conn, 200) =~ note_object.data["content"]
end
- test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
- user = insert(:user)
- data = Map.put(data, "bcc", [user.ap_id])
-
- sender_host = URI.parse(data["actor"]).host
- Instances.set_consistently_unreachable(sender_host)
- refute Instances.reachable?(sender_host)
-
- conn =
- conn
- |> assign(:valid_signature, true)
- |> put_req_header("content-type", "application/activity+json")
- |> post("/users/#{user.nickname}/inbox", data)
-
- assert "ok" == json_response(conn, 200)
- assert Instances.reachable?(sender_host)
- end
-
test "it removes all follower collections but actor's", %{conn: conn} do
[actor, recipient] = insert_pair(:user)
@@ -1320,9 +1267,79 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
html_body: ~r/#{note.data["object"]}/i
)
end
+
+ test "it accepts an incoming Block", %{conn: conn, data: data} do
+ user = insert(:user)
+
+ data =
+ data
+ |> Map.put("type", "Block")
+ |> Map.put("to", [user.ap_id])
+ |> Map.put("cc", [])
+ |> Map.put("object", user.ap_id)
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ assert Activity.get_by_ap_id(data["id"])
+ end
+
+ test "it returns an error when receiving an activity sent to a deactivated user", %{
+ conn: conn,
+ data: data
+ } do
+ user = insert(:user)
+ {:ok, _} = User.set_activation(user, false)
+
+ data =
+ data
+ |> Map.put("bcc", [user.ap_id])
+ |> Kernel.put_in(["object", "bcc"], [user.ap_id])
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "User deactivated" == json_response(conn, 404)
+ end
+
+ test "it returns an error when receiving an activity sent from a deactivated user", %{
+ conn: conn,
+ data: data
+ } do
+ sender = insert(:user)
+ user = insert(:user)
+ {:ok, _} = User.set_activation(sender, false)
+
+ data =
+ data
+ |> Map.put("bcc", [user.ap_id])
+ |> Map.put("actor", sender.ap_id)
+ |> Kernel.put_in(["object", "bcc"], [user.ap_id])
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "Sender deactivated" == json_response(conn, 404)
+ end
end
describe "GET /users/:nickname/outbox" do
+ setup do
+ Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Config)
+ :ok
+ end
+
test "it paginates correctly", %{conn: conn} do
user = insert(:user)
conn = assign(conn, :user, user)
@@ -1411,6 +1428,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert %{"orderedItems" => []} = resp
end
+ test "it does not return a local note activity when C2S API is disabled", %{conn: conn} do
+ clear_config([:activitypub, :client_api_enabled], false)
+ user = insert(:user)
+ reader = insert(:user)
+ {:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"})
+
+ resp =
+ conn
+ |> assign(:user, reader)
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/users/#{user.nickname}/outbox?page=true")
+ |> json_response(200)
+
+ assert %{"orderedItems" => []} = resp
+ end
+
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
@@ -1462,6 +1495,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert [answer_outbox] = outbox_get["orderedItems"]
assert answer_outbox["id"] == activity.data["id"]
end
+
+ test "it works with authorized fetch forced when authenticated" do
+ clear_config([:activitypub, :authorized_fetch_mode], true)
+
+ user = insert(:user)
+ outbox_endpoint = user.ap_id <> "/outbox"
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> put_req_header("accept", "application/activity+json")
+ |> get(outbox_endpoint)
+
+ assert json_response(conn, 200)
+ end
+
+ test "it fails with authorized fetch forced when unauthenticated", %{conn: conn} do
+ clear_config([:activitypub, :authorized_fetch_mode], true)
+
+ user = insert(:user)
+ outbox_endpoint = user.ap_id <> "/outbox"
+
+ conn =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get(outbox_endpoint)
+
+ assert response(conn, 401)
+ end
end
describe "POST /users/:nickname/outbox (C2S)" do
@@ -1623,6 +1685,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert json_response(conn, 403)
end
+ test "it rejects update activity of object from other actor", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ note_object = Object.normalize(note_activity, fetch: false)
+ user = insert(:user)
+
+ data = %{
+ type: "Update",
+ object: %{
+ id: note_object.data["id"]
+ }
+ }
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/outbox", data)
+
+ assert json_response(conn, 400)
+ assert note_object == Object.normalize(note_activity, fetch: false)
+ end
+
test "it increases like count when receiving a like action", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
@@ -2110,6 +2194,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(403)
end
+
+ test "they don't work when C2S API is disabled", %{conn: conn} do
+ clear_config([:activitypub, :client_api_enabled], false)
+
+ user = insert(:user)
+
+ assert conn
+ |> assign(:user, user)
+ |> get("/api/ap/whoami")
+ |> response(403)
+
+ desc = "Description of the image"
+
+ image = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ assert conn
+ |> assign(:user, user)
+ |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
+ |> response(403)
+ end
end
test "pinned collection", %{conn: conn} do
diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs
index b4f6fb68a..c16f081f6 100644
--- a/test/pleroma/web/activity_pub/activity_pub_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_test.exs
@@ -232,12 +232,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert user.avatar == %{
"type" => "Image",
- "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}]
+ "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}],
+ "name" => "profile picture"
}
assert user.banner == %{
"type" => "Image",
- "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}]
+ "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}],
+ "name" => "profile picture"
}
end
@@ -432,6 +434,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert user.birthday == ~D[2001-02-12]
end
+
+ test "fetches avatar description" do
+ user_id = "https://example.com/users/marcin"
+
+ user_data =
+ "test/fixtures/users_mock/user.json"
+ |> File.read!()
+ |> String.replace("{{nickname}}", "marcin")
+ |> Jason.decode!()
+ |> Map.delete("featured")
+ |> Map.update("icon", %{}, fn image -> Map.put(image, "name", "image description") end)
+ |> Jason.encode!()
+
+ Tesla.Mock.mock(fn
+ %{
+ method: :get,
+ url: ^user_id
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: user_data,
+ headers: [{"content-type", "application/activity+json"}]
+ }
+ end)
+
+ {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
+
+ assert user.avatar["name"] == "image description"
+ end
end
test "it fetches the appropriate tag-restricted posts" do
@@ -795,7 +826,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert object.data["repliesCount"] == 2
end
- test "increates quotes count", %{user: user} do
+ test "increases quotes count", %{user: user} do
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"})
@@ -836,6 +867,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end
+ describe "fetch activities for followed hashtags" do
+ test "it should return public activities that reference a given hashtag" do
+ hashtag = insert(:hashtag, name: "tenshi")
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, normally_visible} =
+ CommonAPI.post(other_user, %{status: "hello :)", visibility: "public"})
+
+ {:ok, public} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "public"})
+ {:ok, _unrelated} = CommonAPI.post(user, %{status: "dai #tensh", visibility: "public"})
+ {:ok, unlisted} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "unlisted"})
+ {:ok, _private} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "private"})
+
+ activities =
+ ActivityPub.fetch_activities([other_user.follower_address], %{
+ followed_hashtags: [hashtag.id]
+ })
+
+ assert length(activities) == 3
+ normal_id = normally_visible.id
+ public_id = public.id
+ unlisted_id = unlisted.id
+ assert [%{id: ^normal_id}, %{id: ^public_id}, %{id: ^unlisted_id}] = activities
+ end
+ end
+
describe "fetch activities in context" do
test "retrieves activities that have a given context" do
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
@@ -1727,8 +1785,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
- follower_address: "http://localhost:4001/users/fuser2/followers",
- following_address: "http://localhost:4001/users/fuser2/following"
+ follower_address: "https://remote.org/users/fuser2/followers",
+ following_address: "https://remote.org/users/fuser2/following"
)
{:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
@@ -1739,7 +1797,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
test "detects hidden followers" do
mock(fn env ->
case env.url do
- "http://localhost:4001/users/masto_closed/followers?page=1" ->
+ "https://remote.org/users/masto_closed/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
@@ -1750,8 +1808,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
- follower_address: "http://localhost:4001/users/masto_closed/followers",
- following_address: "http://localhost:4001/users/masto_closed/following"
+ follower_address: "https://remote.org/users/masto_closed/followers",
+ following_address: "https://remote.org/users/masto_closed/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
@@ -1762,7 +1820,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
test "detects hidden follows" do
mock(fn env ->
case env.url do
- "http://localhost:4001/users/masto_closed/following?page=1" ->
+ "https://remote.org/users/masto_closed/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
@@ -1773,8 +1831,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
- follower_address: "http://localhost:4001/users/masto_closed/followers",
- following_address: "http://localhost:4001/users/masto_closed/following"
+ follower_address: "https://remote.org/users/masto_closed/followers",
+ following_address: "https://remote.org/users/masto_closed/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
@@ -1786,8 +1844,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
- follower_address: "http://localhost:8080/followers/fuser3",
- following_address: "http://localhost:8080/following/fuser3"
+ follower_address: "https://remote.org/followers/fuser3",
+ following_address: "https://remote.org/following/fuser3"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
@@ -1800,28 +1858,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
test "doesn't crash when follower and following counters are hidden" do
mock(fn env ->
case env.url do
- "http://localhost:4001/users/masto_hidden_counters/following" ->
+ "https://remote.org/users/masto_hidden_counters/following" ->
json(
%{
"@context" => "https://www.w3.org/ns/activitystreams",
- "id" => "http://localhost:4001/users/masto_hidden_counters/followers"
+ "id" => "https://remote.org/users/masto_hidden_counters/followers"
},
headers: HttpRequestMock.activitypub_object_headers()
)
- "http://localhost:4001/users/masto_hidden_counters/following?page=1" ->
+ "https://remote.org/users/masto_hidden_counters/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
- "http://localhost:4001/users/masto_hidden_counters/followers" ->
+ "https://remote.org/users/masto_hidden_counters/followers" ->
json(
%{
"@context" => "https://www.w3.org/ns/activitystreams",
- "id" => "http://localhost:4001/users/masto_hidden_counters/following"
+ "id" => "https://remote.org/users/masto_hidden_counters/following"
},
headers: HttpRequestMock.activitypub_object_headers()
)
- "http://localhost:4001/users/masto_hidden_counters/followers?page=1" ->
+ "https://remote.org/users/masto_hidden_counters/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
end
end)
@@ -1829,8 +1887,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
- follower_address: "http://localhost:4001/users/masto_hidden_counters/followers",
- following_address: "http://localhost:4001/users/masto_hidden_counters/following"
+ follower_address: "https://remote.org/users/masto_hidden_counters/followers",
+ following_address: "https://remote.org/users/masto_hidden_counters/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
diff --git a/test/pleroma/web/activity_pub/mrf/fo_direct_reply_test.exs b/test/pleroma/web/activity_pub/mrf/fo_direct_reply_test.exs
deleted file mode 100644
index 2d6af3b68..000000000
--- a/test/pleroma/web/activity_pub/mrf/fo_direct_reply_test.exs
+++ /dev/null
@@ -1,117 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ActivityPub.MRF.FODirectReplyTest do
- use Pleroma.DataCase
- import Pleroma.Factory
-
- require Pleroma.Constants
-
- alias Pleroma.Object
- alias Pleroma.Web.ActivityPub.MRF.FODirectReply
- alias Pleroma.Web.CommonAPI
-
- test "replying to followers-only/private is changed to direct" do
- batman = insert(:user, nickname: "batman")
- robin = insert(:user, nickname: "robin")
-
- {:ok, post} =
- CommonAPI.post(batman, %{
- status: "Has anyone seen Selina Kyle's latest selfies?",
- visibility: "private"
- })
-
- reply = %{
- "type" => "Create",
- "actor" => robin.ap_id,
- "to" => [batman.ap_id, robin.follower_address],
- "cc" => [],
- "object" => %{
- "type" => "Note",
- "actor" => robin.ap_id,
- "content" => "@batman 🤤 ❤️ 🐈⬛",
- "to" => [batman.ap_id, robin.follower_address],
- "cc" => [],
- "inReplyTo" => Object.normalize(post).data["id"]
- }
- }
-
- expected_to = [batman.ap_id]
- expected_cc = []
-
- assert {:ok, filtered} = FODirectReply.filter(reply)
-
- assert expected_to == filtered["to"]
- assert expected_cc == filtered["cc"]
- assert expected_to == filtered["object"]["to"]
- assert expected_cc == filtered["object"]["cc"]
- end
-
- test "replies to unlisted posts are unmodified" do
- batman = insert(:user, nickname: "batman")
- robin = insert(:user, nickname: "robin")
-
- {:ok, post} =
- CommonAPI.post(batman, %{
- status: "Has anyone seen Selina Kyle's latest selfies?",
- visibility: "unlisted"
- })
-
- reply = %{
- "type" => "Create",
- "actor" => robin.ap_id,
- "to" => [batman.ap_id, robin.follower_address],
- "cc" => [],
- "object" => %{
- "type" => "Note",
- "actor" => robin.ap_id,
- "content" => "@batman 🤤 ❤️ 🐈<200d>⬛",
- "to" => [batman.ap_id, robin.follower_address],
- "cc" => [],
- "inReplyTo" => Object.normalize(post).data["id"]
- }
- }
-
- assert {:ok, filtered} = FODirectReply.filter(reply)
-
- assert match?(^filtered, reply)
- end
-
- test "replies to public posts are unmodified" do
- batman = insert(:user, nickname: "batman")
- robin = insert(:user, nickname: "robin")
-
- {:ok, post} =
- CommonAPI.post(batman, %{status: "Has anyone seen Selina Kyle's latest selfies?"})
-
- reply = %{
- "type" => "Create",
- "actor" => robin.ap_id,
- "to" => [batman.ap_id, robin.follower_address],
- "cc" => [],
- "object" => %{
- "type" => "Note",
- "actor" => robin.ap_id,
- "content" => "@batman 🤤 ❤️ 🐈<200d>⬛",
- "to" => [batman.ap_id, robin.follower_address],
- "cc" => [],
- "inReplyTo" => Object.normalize(post).data["id"]
- }
- }
-
- assert {:ok, filtered} = FODirectReply.filter(reply)
-
- assert match?(^filtered, reply)
- end
-
- test "non-reply posts are unmodified" do
- batman = insert(:user, nickname: "batman")
-
- {:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"})
-
- assert {:ok, filtered} = FODirectReply.filter(post)
-
- assert match?(^filtered, post)
- end
-end
diff --git a/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs b/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs
index 79e64d650..f66383bf5 100644
--- a/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs
@@ -39,15 +39,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReplyTest do
}
}
- expected_to = [batman.ap_id, robin.follower_address]
- expected_cc = [Pleroma.Constants.as_public()]
-
assert {:ok, filtered} = QuietReply.filter(reply)
- assert expected_to == filtered["to"]
- assert expected_cc == filtered["cc"]
- assert expected_to == filtered["object"]["to"]
- assert expected_cc == filtered["object"]["cc"]
+ assert batman.ap_id in filtered["to"]
+ assert batman.ap_id in filtered["object"]["to"]
+ assert robin.follower_address in filtered["to"]
+ assert robin.follower_address in filtered["object"]["to"]
+ assert Pleroma.Constants.as_public() in filtered["cc"]
+ assert Pleroma.Constants.as_public() in filtered["object"]["cc"]
end
test "replying to unlisted post is unmodified" do
diff --git a/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs b/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs
new file mode 100644
index 000000000..8d2a6b4fa
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs
@@ -0,0 +1,155 @@
+defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicyTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy
+
+ setup do
+ clear_config([:mrf_remote_report, :reject_all], false)
+ end
+
+ test "doesn't impact local report" do
+ clear_config([:mrf_remote_report, :reject_anonymous], true)
+ clear_config([:mrf_remote_report, :reject_empty_message], true)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "http://localhost:4001/actor",
+ "object" => ["https://mastodon.online/users/Gargron"]
+ }
+
+ assert {:ok, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "rejects anonymous report if `reject_anonymous: true`" do
+ clear_config([:mrf_remote_report, :reject_anonymous], true)
+ clear_config([:mrf_remote_report, :reject_empty_message], true)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/actor",
+ "object" => ["https://mastodon.online/users/Gargron"]
+ }
+
+ assert {:reject, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "preserves anonymous report if `reject_anonymous: false`" do
+ clear_config([:mrf_remote_report, :reject_anonymous], false)
+ clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/actor",
+ "object" => ["https://mastodon.online/users/Gargron"]
+ }
+
+ assert {:ok, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "rejects report on third party if `reject_third_party: true`" do
+ clear_config([:mrf_remote_report, :reject_third_party], true)
+ clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/users/Gargron",
+ "object" => ["https://mastodon.online/users/Gargron"]
+ }
+
+ assert {:reject, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "preserves report on first party if `reject_third_party: true`" do
+ clear_config([:mrf_remote_report, :reject_third_party], true)
+ clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/users/Gargron",
+ "object" => ["http://localhost:4001/actor"]
+ }
+
+ assert {:ok, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "preserves report on third party if `reject_third_party: false`" do
+ clear_config([:mrf_remote_report, :reject_third_party], false)
+ clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/users/Gargron",
+ "object" => ["https://mastodon.online/users/Gargron"]
+ }
+
+ assert {:ok, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "rejects empty message report if `reject_empty_message: true`" do
+ clear_config([:mrf_remote_report, :reject_anonymous], false)
+ clear_config([:mrf_remote_report, :reject_empty_message], true)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/users/Gargron",
+ "object" => ["https://mastodon.online/users/Gargron"]
+ }
+
+ assert {:reject, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "rejects empty message report (\"\") if `reject_empty_message: true`" do
+ clear_config([:mrf_remote_report, :reject_anonymous], false)
+ clear_config([:mrf_remote_report, :reject_empty_message], true)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/users/Gargron",
+ "object" => ["https://mastodon.online/users/Gargron"],
+ "content" => ""
+ }
+
+ assert {:reject, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "preserves empty message report if `reject_empty_message: false`" do
+ clear_config([:mrf_remote_report, :reject_anonymous], false)
+ clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/users/Gargron",
+ "object" => ["https://mastodon.online/users/Gargron"]
+ }
+
+ assert {:ok, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "preserves anonymous, empty message report with all settings disabled" do
+ clear_config([:mrf_remote_report, :reject_anonymous], false)
+ clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/actor",
+ "object" => ["https://mastodon.online/users/Gargron"]
+ }
+
+ assert {:ok, _} = RemoteReportPolicy.filter(activity)
+ end
+
+ test "reject remote report if `reject_all: true`" do
+ clear_config([:mrf_remote_report, :reject_all], true)
+ clear_config([:mrf_remote_report, :reject_anonymous], false)
+ clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+ activity = %{
+ "type" => "Flag",
+ "actor" => "https://mastodon.social/users/Gargron",
+ "content" => "Transphobia",
+ "object" => ["https://mastodon.online/users/Gargron"]
+ }
+
+ assert {:reject, _} = RemoteReportPolicy.filter(activity)
+ end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
index 1a51b7d30..f49a7b8ff 100644
--- a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
@@ -252,6 +252,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_message = build_remote_message()
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
+ assert SimplePolicy.id_filter(remote_message["actor"])
end
test "activity has a matching host" do
@@ -260,6 +261,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_message = build_remote_message()
assert {:reject, _} = SimplePolicy.filter(remote_message)
+ refute SimplePolicy.id_filter(remote_message["actor"])
end
test "activity matches with wildcard domain" do
@@ -268,6 +270,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_message = build_remote_message()
assert {:reject, _} = SimplePolicy.filter(remote_message)
+ refute SimplePolicy.id_filter(remote_message["actor"])
end
test "actor has a matching host" do
@@ -276,6 +279,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_user = build_remote_user()
assert {:reject, _} = SimplePolicy.filter(remote_user)
+ refute SimplePolicy.id_filter(remote_user["id"])
end
test "reject Announce when object would be rejected" do
@@ -288,6 +292,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
}
assert {:reject, _} = SimplePolicy.filter(announce)
+ # Note: Non-Applicable for id_filter/1
end
test "reject by URI object" do
@@ -300,6 +305,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
}
assert {:reject, _} = SimplePolicy.filter(announce)
+ # Note: Non-Applicable for id_filter/1
end
end
@@ -370,6 +376,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
+ assert SimplePolicy.id_filter(local_message["actor"])
+ assert SimplePolicy.id_filter(remote_message["actor"])
end
test "is not empty but activity doesn't have a matching host" do
@@ -380,6 +388,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert {:reject, _} = SimplePolicy.filter(remote_message)
+ assert SimplePolicy.id_filter(local_message["actor"])
+ refute SimplePolicy.id_filter(remote_message["actor"])
end
test "activity has a matching host" do
@@ -390,6 +400,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
+ assert SimplePolicy.id_filter(local_message["actor"])
+ assert SimplePolicy.id_filter(remote_message["actor"])
end
test "activity matches with wildcard domain" do
@@ -400,6 +412,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
+ assert SimplePolicy.id_filter(local_message["actor"])
+ assert SimplePolicy.id_filter(remote_message["actor"])
end
test "actor has a matching host" do
@@ -408,6 +422,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_user = build_remote_user()
assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
+ assert SimplePolicy.id_filter(remote_user["id"])
end
end
diff --git a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs
index 2c7497da5..61c162bc9 100644
--- a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs
@@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
assert File.exists?(fullpath)
end
- test "rejects invalid shortcodes", %{path: path} do
+ test "rejects invalid shortcodes with slashes", %{path: path} do
message = %{
"type" => "Create",
"object" => %{
@@ -113,6 +113,58 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
refute File.exists?(fullpath)
end
+ test "rejects invalid shortcodes with dots", %{path: path} do
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "emoji" => [{"fired.fox", "https://example.org/emoji/firedfox"}],
+ "actor" => "https://example.org/users/admin"
+ }
+ }
+
+ fullpath = Path.join(path, "fired.fox.png")
+
+ Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
+ end)
+
+ clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
+
+ refute "fired.fox" in installed()
+ refute File.exists?(path)
+
+ assert {:ok, _message} = StealEmojiPolicy.filter(message)
+
+ refute "fired.fox" in installed()
+ refute File.exists?(fullpath)
+ end
+
+ test "rejects invalid shortcodes with special characters", %{path: path} do
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "emoji" => [{"fired:fox", "https://example.org/emoji/firedfox"}],
+ "actor" => "https://example.org/users/admin"
+ }
+ }
+
+ fullpath = Path.join(path, "fired:fox.png")
+
+ Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
+ end)
+
+ clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
+
+ refute "fired:fox" in installed()
+ refute File.exists?(path)
+
+ assert {:ok, _message} = StealEmojiPolicy.filter(message)
+
+ refute "fired:fox" in installed()
+ refute File.exists?(fullpath)
+ end
+
test "reject regex shortcode", %{message: message} do
refute "firedfox" in installed()
@@ -171,5 +223,74 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
refute "firedfox" in installed()
end
+ test "accepts valid alphanum shortcodes", %{path: path} do
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "emoji" => [{"fire1fox", "https://example.org/emoji/fire1fox.png"}],
+ "actor" => "https://example.org/users/admin"
+ }
+ }
+
+ Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire1fox.png"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
+ end)
+
+ clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
+
+ refute "fire1fox" in installed()
+ refute File.exists?(path)
+
+ assert {:ok, _message} = StealEmojiPolicy.filter(message)
+
+ assert "fire1fox" in installed()
+ end
+
+ test "accepts valid shortcodes with underscores", %{path: path} do
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "emoji" => [{"fire_fox", "https://example.org/emoji/fire_fox.png"}],
+ "actor" => "https://example.org/users/admin"
+ }
+ }
+
+ Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire_fox.png"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
+ end)
+
+ clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
+
+ refute "fire_fox" in installed()
+ refute File.exists?(path)
+
+ assert {:ok, _message} = StealEmojiPolicy.filter(message)
+
+ assert "fire_fox" in installed()
+ end
+
+ test "accepts valid shortcodes with hyphens", %{path: path} do
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "emoji" => [{"fire-fox", "https://example.org/emoji/fire-fox.png"}],
+ "actor" => "https://example.org/users/admin"
+ }
+ }
+
+ Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire-fox.png"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
+ end)
+
+ clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
+
+ refute "fire-fox" in installed()
+ refute File.exists?(path)
+
+ assert {:ok, _message} = StealEmojiPolicy.filter(message)
+
+ assert "fire-fox" in installed()
+ end
+
defp installed, do: Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
end
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index e1dbb20c3..3c7ff0eeb 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -5,12 +5,33 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest do
use Pleroma.DataCase, async: true
+ alias Pleroma.Language.LanguageDetectorMock
+ alias Pleroma.StaticStubbedConfigMock
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
alias Pleroma.Web.ActivityPub.Utils
+ import Mox
import Pleroma.Factory
+ # Setup for all tests
+ setup do
+ # Stub the StaticStubbedConfigMock to return our mock for the provider
+ StaticStubbedConfigMock
+ |> stub(:get, fn
+ [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
+ _other -> nil
+ end)
+
+ # Stub the LanguageDetectorMock with default implementations
+ LanguageDetectorMock
+ |> stub(:missing_dependencies, fn -> [] end)
+ |> stub(:configured?, fn -> true end)
+ |> stub(:detect, fn _text -> nil end)
+
+ :ok
+ end
+
describe "Notes" do
setup do
user = insert(:user)
@@ -128,6 +149,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
+ test "a Note with validated likes collection validates" do
+ insert(:user, ap_id: "https://pol.social/users/mkljczk")
+
+ %{"object" => note} =
+ "test/fixtures/mastodon-update-with-likes.json"
+ |> File.read!()
+ |> Jason.decode!()
+
+ %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
+ end
+
test "Fedibird quote post" do
insert(:user, ap_id: "https://fedibird.com/users/noellabo")
@@ -176,4 +208,102 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
name: "RE: https://server.example/objects/123"
}
end
+
+ describe "Note language" do
+ test "it detects language from JSON-LD context" do
+ user = insert(:user)
+
+ note_activity = %{
+ "@context" => ["https://www.w3.org/ns/activitystreams", %{"@language" => "pl"}],
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "type" => "Create",
+ "object" => %{
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "id" => Utils.generate_object_id(),
+ "type" => "Note",
+ "content" => "Szczęść Boże",
+ "attributedTo" => user.ap_id
+ },
+ "actor" => user.ap_id
+ }
+
+ {:ok, _create_activity, meta} = ObjectValidator.validate(note_activity, [])
+
+ assert meta[:object_data]["language"] == "pl"
+ end
+
+ test "it detects language from contentMap" do
+ user = insert(:user)
+
+ note = %{
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "id" => Utils.generate_object_id(),
+ "type" => "Note",
+ "content" => "Szczęść Boże",
+ "contentMap" => %{
+ "de" => "Gott segne",
+ "pl" => "Szczęść Boże"
+ },
+ "attributedTo" => user.ap_id
+ }
+
+ {:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
+
+ assert object.language == "pl"
+ end
+
+ test "it doesn't call LanguageDetector when language is specified" do
+ # Set up expectation that detect should not be called
+ LanguageDetectorMock
+ |> expect(:detect, 0, fn _ -> flunk("LanguageDetector.detect should not be called") end)
+ |> stub(:missing_dependencies, fn -> [] end)
+ |> stub(:configured?, fn -> true end)
+
+ # Stub the StaticStubbedConfigMock to return our mock for the provider
+ StaticStubbedConfigMock
+ |> stub(:get, fn
+ [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
+ _other -> nil
+ end)
+
+ user = insert(:user)
+
+ note = %{
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "id" => Utils.generate_object_id(),
+ "type" => "Note",
+ "content" => "a post in English",
+ "contentMap" => %{
+ "en" => "a post in English"
+ },
+ "attributedTo" => user.ap_id
+ }
+
+ ArticleNotePageValidator.cast_and_apply(note)
+ end
+
+ test "it adds contentMap if language is specified" do
+ user = insert(:user)
+
+ note = %{
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "id" => Utils.generate_object_id(),
+ "type" => "Note",
+ "content" => "тест",
+ "language" => "uk",
+ "attributedTo" => user.ap_id
+ }
+
+ {:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
+
+ assert object.contentMap == %{
+ "uk" => "тест"
+ }
+ end
+ end
end
diff --git a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs
index 6627fa6db..744ae8704 100644
--- a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs
@@ -13,6 +13,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do
import Pleroma.Factory
describe "attachments" do
+ test "works with apng" do
+ attachment =
+ %{
+ "mediaType" => "image/apng",
+ "name" => "",
+ "type" => "Document",
+ "url" =>
+ "https://media.misskeyusercontent.com/io/2859c26e-cd43-4550-848b-b6243bc3fe28.apng"
+ }
+
+ assert {:ok, attachment} =
+ AttachmentValidator.cast_and_validate(attachment)
+ |> Ecto.Changeset.apply_action(:insert)
+
+ assert attachment.mediaType == "image/apng"
+ end
+
test "fails without url" do
attachment = %{
"mediaType" => "",
diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs
index 99ed42877..44e81377e 100644
--- a/test/pleroma/web/activity_pub/publisher_test.exs
+++ b/test/pleroma/web/activity_pub/publisher_test.exs
@@ -6,13 +6,11 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.Web.ConnCase
- import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
import Mock
alias Pleroma.Activity
- alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.ActivityPub.Publisher
@@ -167,115 +165,6 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
})
|> Publisher.publish_one()
end
-
- test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set",
- Instances,
- [:passthrough],
- [] do
- _actor = insert(:user)
- inbox = "http://200.site/users/nick1/inbox"
- activity = insert(:note_activity)
-
- assert {:ok, _} =
- Publisher.prepare_one(%{
- inbox: inbox,
- activity_id: activity.id,
- unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.to_string()
- })
- |> Publisher.publish_one()
-
- assert called(Instances.set_reachable(inbox))
- end
-
- test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil",
- Instances,
- [:passthrough],
- [] do
- _actor = insert(:user)
- inbox = "http://200.site/users/nick1/inbox"
- activity = insert(:note_activity)
-
- assert {:ok, _} =
- Publisher.prepare_one(%{
- inbox: inbox,
- activity_id: activity.id,
- unreachable_since: nil
- })
- |> Publisher.publish_one()
-
- refute called(Instances.set_reachable(inbox))
- end
-
- test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
- Instances,
- [:passthrough],
- [] do
- _actor = insert(:user)
- inbox = "http://404.site/users/nick1/inbox"
- activity = insert(:note_activity)
-
- assert {:cancel, _} =
- Publisher.prepare_one(%{inbox: inbox, activity_id: activity.id})
- |> Publisher.publish_one()
-
- assert called(Instances.set_unreachable(inbox))
- end
-
- test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
- Instances,
- [:passthrough],
- [] do
- _actor = insert(:user)
- inbox = "http://connrefused.site/users/nick1/inbox"
- activity = insert(:note_activity)
-
- assert capture_log(fn ->
- assert {:error, _} =
- Publisher.prepare_one(%{
- inbox: inbox,
- activity_id: activity.id
- })
- |> Publisher.publish_one()
- end) =~ "connrefused"
-
- assert called(Instances.set_unreachable(inbox))
- end
-
- test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable",
- Instances,
- [:passthrough],
- [] do
- _actor = insert(:user)
- inbox = "http://200.site/users/nick1/inbox"
- activity = insert(:note_activity)
-
- assert {:ok, _} =
- Publisher.prepare_one(%{inbox: inbox, activity_id: activity.id})
- |> Publisher.publish_one()
-
- refute called(Instances.set_unreachable(inbox))
- end
-
- test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`",
- Instances,
- [:passthrough],
- [] do
- _actor = insert(:user)
- inbox = "http://connrefused.site/users/nick1/inbox"
- activity = insert(:note_activity)
-
- assert capture_log(fn ->
- assert {:error, _} =
- Publisher.prepare_one(%{
- inbox: inbox,
- activity_id: activity.id,
- unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.to_string()
- })
- |> Publisher.publish_one()
- end) =~ "connrefused"
-
- refute called(Instances.set_unreachable(inbox))
- end
end
describe "publish/2" do
@@ -520,4 +409,105 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert decoded["cc"] == []
end
+
+ test "unlisted activities retain public address in cc" do
+ user = insert(:user)
+
+ # simulate unlistd activity by only having
+ # public address in cc
+ activity =
+ insert(:note_activity,
+ user: user,
+ data_attrs: %{
+ "cc" => [@as_public],
+ "to" => [user.follower_address]
+ }
+ )
+
+ assert @as_public in activity.data["cc"]
+
+ prepared =
+ Publisher.prepare_one(%{
+ inbox: "https://remote.instance/users/someone/inbox",
+ activity_id: activity.id
+ })
+
+ {:ok, decoded} = Jason.decode(prepared.json)
+
+ assert @as_public in decoded["cc"]
+
+ # maybe we also have another inbox in cc
+ # during Publishing
+ activity =
+ insert(:note_activity,
+ user: user,
+ data_attrs: %{
+ "cc" => [@as_public],
+ "to" => [user.follower_address]
+ }
+ )
+
+ prepared =
+ Publisher.prepare_one(%{
+ inbox: "https://remote.instance/users/someone/inbox",
+ activity_id: activity.id,
+ cc: ["https://remote.instance/users/someone_else/inbox"]
+ })
+
+ {:ok, decoded} = Jason.decode(prepared.json)
+
+ assert decoded["cc"] == [@as_public, "https://remote.instance/users/someone_else/inbox"]
+ end
+
+ test "public address in cc parameter is preserved" do
+ user = insert(:user)
+
+ cc_with_public = [@as_public, "https://example.org/users/other"]
+
+ activity =
+ insert(:note_activity,
+ user: user,
+ data_attrs: %{
+ "cc" => cc_with_public,
+ "to" => [user.follower_address]
+ }
+ )
+
+ assert @as_public in activity.data["cc"]
+
+ prepared =
+ Publisher.prepare_one(%{
+ inbox: "https://remote.instance/users/someone/inbox",
+ activity_id: activity.id,
+ cc: cc_with_public
+ })
+
+ {:ok, decoded} = Jason.decode(prepared.json)
+
+ assert cc_with_public == decoded["cc"]
+ end
+
+ test "cc parameter is preserved" do
+ user = insert(:user)
+
+ activity =
+ insert(:note_activity,
+ user: user,
+ data_attrs: %{
+ "cc" => ["https://example.com/specific/user"],
+ "to" => [user.follower_address]
+ }
+ )
+
+ prepared =
+ Publisher.prepare_one(%{
+ inbox: "https://remote.instance/users/someone/inbox",
+ activity_id: activity.id,
+ cc: ["https://example.com/specific/user"]
+ })
+
+ {:ok, decoded} = Jason.decode(prepared.json)
+
+ assert decoded["cc"] == ["https://example.com/specific/user"]
+ end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs
index c02f66d77..27f8522ce 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do
use Pleroma.DataCase, async: true
alias Pleroma.Activity
+ alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
@@ -75,4 +76,107 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do
assert activity_data["object"] == activity.data["object"]
assert activity_data["content"] == "⭐"
end
+
+ test "it works for misskey likes with custom emoji" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "hello"})
+
+ data =
+ File.read!("test/fixtures/misskey-custom-emoji-like.json")
+ |> Jason.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert activity_data["actor"] == data["actor"]
+ assert activity_data["type"] == "EmojiReact"
+ assert activity_data["id"] == data["id"]
+ assert activity_data["object"] == activity.data["object"]
+ assert activity_data["content"] == ":blobwtfnotlikethis:"
+
+ assert [["blobwtfnotlikethis", _, _]] =
+ Object.get_by_ap_id(activity.data["object"])
+ |> Object.get_emoji_reactions()
+ end
+
+ test "it works for mitra likes with custom emoji" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "hello"})
+
+ data =
+ File.read!("test/fixtures/mitra-custom-emoji-like.json")
+ |> Jason.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert activity_data["actor"] == data["actor"]
+ assert activity_data["type"] == "EmojiReact"
+ assert activity_data["id"] == data["id"]
+ assert activity_data["object"] == activity.data["object"]
+ assert activity_data["content"] == ":ablobcatheartsqueeze:"
+
+ assert [["ablobcatheartsqueeze", _, _]] =
+ Object.get_by_ap_id(activity.data["object"])
+ |> Object.get_emoji_reactions()
+ end
+
+ test "it works for likes with wrong content" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "hello"})
+
+ data =
+ File.read!("test/fixtures/mitra-custom-emoji-like.json")
+ |> Jason.decode!()
+ |> Map.put("object", activity.data["object"])
+ |> Map.put("content", 1)
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(data)
+ assert activity.data["type"] == "Like"
+ end
+
+ test "it changes incoming dislikes into emoji reactions" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "hello"})
+
+ data =
+ File.read!("test/fixtures/friendica-dislike.json")
+ |> Jason.decode!()
+ |> Map.put("object", activity.data["object"])
+
+ _actor = insert(:user, ap_id: data["actor"], local: false)
+
+ {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+ refute Enum.empty?(activity.recipients)
+
+ assert data["actor"] == "https://my-place.social/profile/vaartis"
+ assert data["type"] == "EmojiReact"
+ assert data["content"] == "👎"
+ assert data["id"] == "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182"
+ assert data["object"] == activity.data["object"]
+
+ data =
+ File.read!("test/fixtures/friendica-dislike-undo.json")
+ |> Jason.decode!()
+ |> put_in(["object", "object"], activity.data["object"])
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["actor"] == "https://my-place.social/profile/vaartis"
+ assert data["type"] == "Undo"
+
+ assert data["object"] ==
+ "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182"
+ end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
index ed71dcb90..fd7a3c772 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
@@ -219,6 +219,36 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
"@lain
"
end
+ test "it only uses contentMap if content is not present" do
+ user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "type" => "Create",
+ "object" => %{
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "id" => Utils.generate_object_id(),
+ "type" => "Note",
+ "content" => "Hi",
+ "contentMap" => %{
+ "de" => "Hallo",
+ "uk" => "Привіт"
+ },
+ "inReplyTo" => nil,
+ "attributedTo" => user.ap_id
+ },
+ "actor" => user.ap_id
+ }
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(message)
+ object = Object.normalize(data["object"], fetch: false)
+
+ assert object.data["content"] == "Hi"
+ end
+
test "it works for incoming notices with a nil contentMap (firefish)" do
data =
File.read!("test/fixtures/mastodon-post-activity-contentmap.json")
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 6da7e4a89..e0395d7bb 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -156,6 +156,246 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
# It fetched the quoted post
assert Object.normalize("https://misskey.io/notes/8vs6wxufd0")
end
+
+ test "doesn't allow remote edits to fake local likes" do
+ # as a spot check for no internal fields getting injected
+ now = DateTime.utc_now()
+ pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
+ edit_date = DateTime.to_iso8601(now)
+
+ local_user = insert(:user)
+
+ create_data = %{
+ "type" => "Create",
+ "id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity",
+ "actor" => "http://mastodon.example.org/users/admin",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "http://mastodon.example.org/users/admin/statuses/2619539638",
+ "attributedTo" => "http://mastodon.example.org/users/admin",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "published" => pub_date,
+ "content" => "miaow",
+ "likes" => [local_user.ap_id]
+ }
+ }
+
+ update_data =
+ create_data
+ |> Map.put("type", "Update")
+ |> Map.put("id", create_data["object"]["id"] <> "/update/1")
+ |> put_in(["object", "content"], "miaow :3")
+ |> put_in(["object", "updated"], edit_date)
+ |> put_in(["object", "formerRepresentations"], %{
+ "type" => "OrderedCollection",
+ "totalItems" => 1,
+ "orderedItems" => [create_data["object"]]
+ })
+
+ {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
+ %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
+ assert object.data["content"] == "miaow"
+ assert object.data["likes"] == []
+ assert object.data["like_count"] == 0
+
+ {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
+ %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
+ assert object.data["content"] == "miaow :3"
+ assert object.data["likes"] == []
+ assert object.data["like_count"] == 0
+ end
+
+ test "strips internal fields from history items in edited notes" do
+ now = DateTime.utc_now()
+ pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
+ edit_date = DateTime.to_iso8601(now)
+
+ local_user = insert(:user)
+
+ create_data = %{
+ "type" => "Create",
+ "id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity",
+ "actor" => "http://mastodon.example.org/users/admin",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "http://mastodon.example.org/users/admin/statuses/2619539638",
+ "attributedTo" => "http://mastodon.example.org/users/admin",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "published" => pub_date,
+ "content" => "miaow",
+ "likes" => [],
+ "like_count" => 0
+ }
+ }
+
+ update_data =
+ create_data
+ |> Map.put("type", "Update")
+ |> Map.put("id", create_data["object"]["id"] <> "/update/1")
+ |> put_in(["object", "content"], "miaow :3")
+ |> put_in(["object", "updated"], edit_date)
+ |> put_in(["object", "formerRepresentations"], %{
+ "type" => "OrderedCollection",
+ "totalItems" => 1,
+ "orderedItems" => [
+ Map.merge(create_data["object"], %{
+ "likes" => [local_user.ap_id],
+ "like_count" => 1,
+ "pleroma" => %{"internal_field" => "should_be_stripped"}
+ })
+ ]
+ })
+
+ {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
+ %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
+ assert object.data["content"] == "miaow"
+ assert object.data["likes"] == []
+ assert object.data["like_count"] == 0
+
+ {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
+ %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
+ assert object.data["content"] == "miaow :3"
+ assert object.data["likes"] == []
+ assert object.data["like_count"] == 0
+
+ # Check that internal fields are stripped from history items
+ history_item = List.first(object.data["formerRepresentations"]["orderedItems"])
+ assert history_item["likes"] == []
+ assert history_item["like_count"] == 0
+ refute Map.has_key?(history_item, "pleroma")
+ end
+
+ test "doesn't trip over remote likes in notes" do
+ now = DateTime.utc_now()
+ pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
+ edit_date = DateTime.to_iso8601(now)
+
+ create_data = %{
+ "type" => "Create",
+ "id" => "http://mastodon.example.org/users/admin/statuses/3409297097/activity",
+ "actor" => "http://mastodon.example.org/users/admin",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "http://mastodon.example.org/users/admin/statuses/3409297097",
+ "attributedTo" => "http://mastodon.example.org/users/admin",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "published" => pub_date,
+ "content" => "miaow",
+ "likes" => %{
+ "id" => "http://mastodon.example.org/users/admin/statuses/3409297097/likes",
+ "totalItems" => 0,
+ "type" => "Collection"
+ }
+ }
+ }
+
+ update_data =
+ create_data
+ |> Map.put("type", "Update")
+ |> Map.put("id", create_data["object"]["id"] <> "/update/1")
+ |> put_in(["object", "content"], "miaow :3")
+ |> put_in(["object", "updated"], edit_date)
+ |> put_in(["object", "likes", "totalItems"], 666)
+ |> put_in(["object", "formerRepresentations"], %{
+ "type" => "OrderedCollection",
+ "totalItems" => 1,
+ "orderedItems" => [create_data["object"]]
+ })
+
+ {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
+ %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
+ assert object.data["content"] == "miaow"
+ assert object.data["likes"] == []
+ assert object.data["like_count"] == 0
+
+ {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
+ %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
+ assert object.data["content"] == "miaow :3"
+ assert object.data["likes"] == []
+ # in the future this should retain remote likes, but for now:
+ assert object.data["like_count"] == 0
+ end
+
+ test "doesn't trip over remote likes in polls" do
+ now = DateTime.utc_now()
+ pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
+ edit_date = DateTime.to_iso8601(now)
+
+ create_data = %{
+ "type" => "Create",
+ "id" => "http://mastodon.example.org/users/admin/statuses/2471790073/activity",
+ "actor" => "http://mastodon.example.org/users/admin",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Question",
+ "id" => "http://mastodon.example.org/users/admin/statuses/2471790073",
+ "attributedTo" => "http://mastodon.example.org/users/admin",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "published" => pub_date,
+ "content" => "vote!",
+ "anyOf" => [
+ %{
+ "type" => "Note",
+ "name" => "a",
+ "replies" => %{
+ "type" => "Collection",
+ "totalItems" => 3
+ }
+ },
+ %{
+ "type" => "Note",
+ "name" => "b",
+ "replies" => %{
+ "type" => "Collection",
+ "totalItems" => 1
+ }
+ }
+ ],
+ "likes" => %{
+ "id" => "http://mastodon.example.org/users/admin/statuses/2471790073/likes",
+ "totalItems" => 0,
+ "type" => "Collection"
+ }
+ }
+ }
+
+ update_data =
+ create_data
+ |> Map.put("type", "Update")
+ |> Map.put("id", create_data["object"]["id"] <> "/update/1")
+ |> put_in(["object", "content"], "vote now!")
+ |> put_in(["object", "updated"], edit_date)
+ |> put_in(["object", "likes", "totalItems"], 666)
+ |> put_in(["object", "formerRepresentations"], %{
+ "type" => "OrderedCollection",
+ "totalItems" => 1,
+ "orderedItems" => [create_data["object"]]
+ })
+
+ {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
+ %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
+ assert object.data["content"] == "vote!"
+ assert object.data["likes"] == []
+ assert object.data["like_count"] == 0
+
+ {:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
+ %Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
+ assert object.data["content"] == "vote now!"
+ assert object.data["likes"] == []
+ # in the future this should retain remote likes, but for now:
+ assert object.data["like_count"] == 0
+ end
end
describe "prepare outgoing" do
@@ -384,6 +624,24 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert modified["object"]["quoteUrl"] == quote_id
assert modified["object"]["quoteUri"] == quote_id
end
+
+ test "it adds language of the object to its json-ld context" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
+ {:ok, modified} = Transmogrifier.prepare_outgoing(activity.object.data)
+
+ assert [_, _, %{"@language" => "pl"}] = modified["@context"]
+ end
+
+ test "it adds language of the object to Create activity json-ld context" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
+ {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+
+ assert [_, _, %{"@language" => "pl"}] = modified["@context"]
+ end
end
describe "actor rewriting" do
@@ -621,5 +879,14 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
processed = Transmogrifier.prepare_object(original)
assert processed["formerRepresentations"] == original["formerRepresentations"]
end
+
+ test "it uses contentMap to specify post language" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
+ object = Transmogrifier.prepare_object(activity.object.data)
+
+ assert %{"contentMap" => %{"pl" => "Cześć"}} = object
+ end
end
end
diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs
index 872a440cb..45fef154e 100644
--- a/test/pleroma/web/activity_pub/utils_test.exs
+++ b/test/pleroma/web/activity_pub/utils_test.exs
@@ -173,16 +173,30 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
end
end
- test "make_json_ld_header/0" do
- assert Utils.make_json_ld_header() == %{
- "@context" => [
- "https://www.w3.org/ns/activitystreams",
- "http://localhost:4001/schemas/litepub-0.1.jsonld",
- %{
- "@language" => "und"
- }
- ]
- }
+ describe "make_json_ld_header/1" do
+ test "makes jsonld header" do
+ assert Utils.make_json_ld_header() == %{
+ "@context" => [
+ "https://www.w3.org/ns/activitystreams",
+ "http://localhost:4001/schemas/litepub-0.1.jsonld",
+ %{
+ "@language" => "und"
+ }
+ ]
+ }
+ end
+
+ test "includes language if specified" do
+ assert Utils.make_json_ld_header(%{"language" => "pl"}) == %{
+ "@context" => [
+ "https://www.w3.org/ns/activitystreams",
+ "http://localhost:4001/schemas/litepub-0.1.jsonld",
+ %{
+ "@language" => "pl"
+ }
+ ]
+ }
+ end
end
describe "get_existing_votes" do
diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs
index 651e535ac..a32e72829 100644
--- a/test/pleroma/web/activity_pub/views/user_view_test.exs
+++ b/test/pleroma/web/activity_pub/views/user_view_test.exs
@@ -68,6 +68,23 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
result = UserView.render("user.json", %{user: user})
assert result["icon"]["url"] == "https://someurl"
assert result["image"]["url"] == "https://somebanner"
+
+ refute result["icon"]["name"]
+ refute result["image"]["name"]
+ end
+
+ test "Avatar has a description if the user set one" do
+ user =
+ insert(:user,
+ avatar: %{
+ "url" => [%{"href" => "https://someurl"}],
+ "name" => "a drawing of pleroma-tan using pleroma groups"
+ }
+ )
+
+ result = UserView.render("user.json", %{user: user})
+
+ assert result["icon"]["name"] == "a drawing of pleroma-tan using pleroma groups"
end
test "renders an invisible user with the invisible property set to true" do
diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs
index dc12155f5..e12115ea1 100644
--- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs
@@ -1211,8 +1211,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
end
test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do
- clear_config(Pleroma.Upload.Filter.Mogrify)
-
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{
@@ -1240,7 +1238,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"need_reboot" => false
}
- assert Config.get(Pleroma.Upload.Filter.Mogrify) == [args: ["auto-orient", "strip"]]
+ config = Config.get(Pleroma.Upload.Filter.Mogrify)
+ assert {:args, ["auto-orient", "strip"]} in config
assert conn
|> put_req_header("content-type", "application/json")
@@ -1289,9 +1288,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"need_reboot" => false
}
- assert Config.get(Pleroma.Upload.Filter.Mogrify) == [
- args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}]
- ]
+ config = Config.get(Pleroma.Upload.Filter.Mogrify)
+
+ assert {:args, ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}]} in config
end
test "enables the welcome messages", %{conn: conn} do
diff --git a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
index 0d1a4999e..a6b8dba46 100644
--- a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.FrontendControllerTest do
setup do
clear_config([:instance, :static_dir], @dir)
- File.mkdir_p!(Pleroma.Frontend.dir())
+ Pleroma.Backports.mkdir_p!(Pleroma.Frontend.dir())
on_exit(fn ->
File.rm_rf(@dir)
diff --git a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
index 6cca623f3..5adcd069d 100644
--- a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
@@ -8,8 +8,6 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do
import Pleroma.Factory
- alias Pleroma.Repo
- alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.CommonAPI
setup_all do
@@ -69,19 +67,19 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do
test "DELETE /instances/:instance", %{conn: conn} do
clear_config([:instance, :admin_privileges], [:instances_delete])
- user = insert(:user, nickname: "lain@lain.com")
- post = insert(:note_activity, user: user)
+ insert(:user, nickname: "lain@lain.com")
response =
conn
|> delete("/api/pleroma/admin/instances/lain.com")
|> json_response(200)
- [:ok] = ObanHelpers.perform_all()
-
assert response == "lain.com"
- refute Repo.reload(user).is_active
- refute Repo.reload(post)
+
+ assert_enqueued(
+ worker: Pleroma.Workers.DeleteWorker,
+ args: %{"op" => "delete_instance", "host" => "lain.com"}
+ )
clear_config([:instance, :admin_privileges], [])
diff --git a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs
index 9511dccea..344c908fe 100644
--- a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do
@default_instance_panel ~s(Welcome to Pleroma!
)
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
on_exit(fn -> File.rm_rf(@dir) end)
end
diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs
index c8495c477..0e5650285 100644
--- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do
alias Pleroma.Web.MediaProxy
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 73230a58c..6b5d31537 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -111,6 +111,17 @@ defmodule Pleroma.Web.CommonAPITest do
end
end
+ test "add expiring block", %{blocker: blocker, blocked: blocked} do
+ {:ok, _} = CommonAPI.block(blocked, blocker, %{expires_in: 60})
+ assert User.blocks?(blocker, blocked)
+
+ worker = Pleroma.Workers.MuteExpireWorker
+ args = %{"op" => "unblock_user", "blocker_id" => blocker.id, "blocked_id" => blocked.id}
+
+ assert :ok = perform_job(worker, args)
+ refute User.blocks?(blocker, blocked)
+ end
+
test "it blocks and does not federate if outgoing blocks are disabled", %{
blocker: blocker,
blocked: blocked
diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs
index ed34d6490..9184cf8f1 100644
--- a/test/pleroma/web/fallback_test.exs
+++ b/test/pleroma/web/fallback_test.exs
@@ -32,7 +32,7 @@ defmodule Pleroma.Web.FallbackTest do
resp = get(conn, "/foo")
assert html_response(resp, 200) =~ "a cool title "
- refute html_response(resp, 200) =~ "initial-results"
+ assert html_response(resp, 200) =~ " "
end
test "GET /*path", %{conn: conn} do
diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs
index 4a398f239..16fe1066a 100644
--- a/test/pleroma/web/federator_test.exs
+++ b/test/pleroma/web/federator_test.exs
@@ -126,22 +126,17 @@ defmodule Pleroma.Web.FederatorTest do
inbox: inbox2
})
- dt = NaiveDateTime.utc_now()
- Instances.set_unreachable(inbox1, dt)
-
- Instances.set_consistently_unreachable(URI.parse(inbox2).host)
+ Instances.set_unreachable(URI.parse(inbox2).host)
{:ok, _activity} =
CommonAPI.post(user, %{status: "HI @nick1@domain.com, @nick2@domain2.com!"})
- expected_dt = NaiveDateTime.to_iso8601(dt)
-
ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
assert ObanHelpers.member?(
%{
"op" => "publish_one",
- "params" => %{"inbox" => inbox1, "unreachable_since" => expected_dt}
+ "params" => %{"inbox" => inbox1}
},
all_enqueued(worker: PublisherWorker)
)
diff --git a/test/pleroma/web/feed/tag_controller_test.exs b/test/pleroma/web/feed/tag_controller_test.exs
index 7d196b228..662235f31 100644
--- a/test/pleroma/web/feed/tag_controller_test.exs
+++ b/test/pleroma/web/feed/tag_controller_test.exs
@@ -191,4 +191,60 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|> response(404)
end
end
+
+ describe "restricted for unauthenticated" do
+ test "returns 404 when local timeline is disabled", %{conn: conn} do
+ clear_config([:restrict_unauthenticated, :timelines], %{local: true, federated: false})
+
+ conn
+ |> put_req_header("accept", "application/rss+xml")
+ |> get(tag_feed_path(conn, :feed, "pleromaart.rss"))
+ |> response(404)
+ end
+
+ test "returns local posts only when federated timeline is disabled", %{conn: conn} do
+ clear_config([:restrict_unauthenticated, :timelines], %{local: false, federated: true})
+
+ local_user = insert(:user)
+ remote_user = insert(:user, local: false)
+
+ local_note =
+ insert(:note,
+ user: local_user,
+ data: %{
+ "content" => "local post #PleromaArt",
+ "summary" => "",
+ "tag" => ["pleromaart"]
+ }
+ )
+
+ remote_note =
+ insert(:note,
+ user: remote_user,
+ data: %{
+ "content" => "remote post #PleromaArt",
+ "summary" => "",
+ "tag" => ["pleromaart"]
+ },
+ local: false
+ )
+
+ insert(:note_activity, user: local_user, note: local_note)
+ insert(:note_activity, user: remote_user, note: remote_note, local: false)
+
+ response =
+ conn
+ |> put_req_header("accept", "application/rss+xml")
+ |> get(tag_feed_path(conn, :feed, "pleromaart.rss"))
+ |> response(200)
+
+ xml = parse(response)
+
+ assert xpath(xml, ~x"//channel/title/text()") == ~c"#pleromaart"
+
+ assert xpath(xml, ~x"//channel/item/title/text()"l) == [
+ ~c"local post #PleromaArt"
+ ]
+ end
+ end
end
diff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs
index 1c17d47b4..0a3aaff5c 100644
--- a/test/pleroma/web/feed/user_controller_test.exs
+++ b/test/pleroma/web/feed/user_controller_test.exs
@@ -147,6 +147,15 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
assert response(conn, 404)
end
+ test "returns noindex meta for missing user", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("accept", "text/html")
+ |> get("/users/nonexisting")
+
+ assert html_response(conn, 200) =~ " "
+ end
+
test "returns feed with public and unlisted activities", %{conn: conn} do
user = insert(:user)
diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
index 54f6818bd..cd3107f32 100644
--- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
@@ -19,7 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
import Pleroma.Factory
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
index 373a84303..8a0fe5259 100644
--- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
@@ -152,4 +152,46 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
}
] = result["rules"]
end
+
+ test "translation languages matrix", %{conn: conn} do
+ clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
+
+ assert %{"en" => ["pl"], "pl" => ["en"]} =
+ conn
+ |> get("/api/v1/instance/translation_languages")
+ |> json_response_and_validate_schema(200)
+ end
+
+ test "base_urls in pleroma metadata", %{conn: conn} do
+ media_proxy_base_url = "https://media.example.org"
+ upload_base_url = "https://uploads.example.org"
+
+ clear_config([:media_proxy, :enabled], true)
+ clear_config([:media_proxy, :base_url], media_proxy_base_url)
+ clear_config([Pleroma.Upload, :base_url], upload_base_url)
+
+ conn = get(conn, "/api/v1/instance")
+
+ assert result = json_response_and_validate_schema(conn, 200)
+ assert result["pleroma"]["metadata"]["base_urls"]["media_proxy"] == media_proxy_base_url
+ assert result["pleroma"]["metadata"]["base_urls"]["upload"] == upload_base_url
+
+ # Test when media_proxy is disabled
+ clear_config([:media_proxy, :enabled], false)
+
+ conn = get(conn, "/api/v1/instance")
+
+ assert result = json_response_and_validate_schema(conn, 200)
+ refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "media_proxy")
+ assert result["pleroma"]["metadata"]["base_urls"]["upload"] == upload_base_url
+
+ # Test when upload base_url is not set
+ clear_config([Pleroma.Upload, :base_url], nil)
+
+ conn = get(conn, "/api/v1/instance")
+
+ assert result = json_response_and_validate_schema(conn, 200)
+ refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "media_proxy")
+ refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "upload")
+ end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs
index 4adbaa640..ae86078d7 100644
--- a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs
@@ -56,7 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v2/media", %{"file" => image, "description" => desc})
- |> json_response_and_validate_schema(202)
+ |> json_response_and_validate_schema(200)
assert media_id = response["id"]
@@ -111,7 +111,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
"file" => large_binary,
"description" => desc
})
- |> json_response_and_validate_schema(202)
+ |> json_response_and_validate_schema(200)
assert media_id = response["id"]
@@ -227,4 +227,93 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
|> json_response_and_validate_schema(403)
end
end
+
+ describe "Content-Type sanitization" do
+ setup do: oauth_access(["write:media", "read:media"])
+
+ setup do
+ ConfigMock
+ |> stub_with(Pleroma.Test.StaticConfig)
+
+ config =
+ Pleroma.Config.get([Pleroma.Upload])
+ |> Keyword.put(:uploader, Pleroma.Uploaders.Local)
+
+ clear_config([Pleroma.Upload], config)
+ clear_config([Pleroma.Upload, :allowed_mime_types], ["image", "audio", "video"])
+
+ # Create a file with a malicious content type and dangerous extension
+ malicious_file = %Plug.Upload{
+ content_type: "application/activity+json",
+ path: Path.absname("test/fixtures/image.jpg"),
+ # JSON extension to make MIME.from_path detect application/json
+ filename: "malicious.json"
+ }
+
+ [malicious_file: malicious_file]
+ end
+
+ test "sanitizes malicious content types when serving media", %{
+ conn: conn,
+ malicious_file: malicious_file
+ } do
+ # First upload the file with the malicious content type
+ media =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/v1/media", %{"file" => malicious_file})
+ |> json_response_and_validate_schema(:ok)
+
+ # Get the file URL from the response
+ url = media["url"]
+
+ # Now make a direct request to the media URL and check the content-type header
+ response =
+ build_conn()
+ |> get(URI.parse(url).path)
+
+ # Find the content-type header
+ content_type_header =
+ Enum.find(response.resp_headers, fn {name, _} -> name == "content-type" end)
+
+ # The server should detect the application/json MIME type from the .json extension
+ # and replace it with application/octet-stream since it's not in allowed_mime_types
+ assert content_type_header == {"content-type", "application/octet-stream"}
+
+ # Verify that the file was still served correctly
+ assert response.status == 200
+ end
+
+ test "allows safe content types", %{conn: conn} do
+ safe_image = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "safe_image.jpg"
+ }
+
+ # Upload a file with a safe content type
+ media =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/v1/media", %{"file" => safe_image})
+ |> json_response_and_validate_schema(:ok)
+
+ # Get the file URL from the response
+ url = media["url"]
+
+ # Make a direct request to the media URL and check the content-type header
+ response =
+ build_conn()
+ |> get(URI.parse(url).path)
+
+ # The server should preserve the image/jpeg MIME type since it's allowed
+ content_type_header =
+ Enum.find(response.resp_headers, fn {name, _} -> name == "content-type" end)
+
+ assert content_type_header == {"content-type", "image/jpeg"}
+
+ # Verify that the file was served correctly
+ assert response.status == 200
+ end
+ end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs
index 8fc22dde1..88f2fb7af 100644
--- a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
import Pleroma.Factory
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs
index 7912b1d5f..51af87742 100644
--- a/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
+ use Oban.Testing, repo: Pleroma.Repo
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Object
@@ -27,6 +28,33 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do
response = json_response_and_validate_schema(conn, 200)
id = to_string(object.id)
assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
+
+ # Local activities should not generate an Oban job to refresh
+ assert activity.local
+
+ refute_enqueued(
+ worker: Pleroma.Workers.PollWorker,
+ args: %{"op" => "refresh", "activity_id" => activity.id}
+ )
+ end
+
+ test "creates an oban job to refresh poll if activity is remote", %{conn: conn} do
+ user = insert(:user, local: false)
+ question = insert(:question, user: user)
+ activity = insert(:question_activity, question: question, local: false)
+
+ # Ensure this is not represented as a local activity
+ refute activity.local
+
+ object = Object.normalize(activity, fetch: false)
+
+ get(conn, "/api/v1/polls/#{object.id}")
+ |> json_response_and_validate_schema(200)
+
+ assert_enqueued(
+ worker: Pleroma.Workers.PollWorker,
+ args: %{"op" => "refresh", "activity_id" => activity.id}
+ )
end
test "does not expose polls for private statuses", %{conn: conn} do
diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs
index d38767c96..f0c9c1901 100644
--- a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs
@@ -7,14 +7,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
alias Pleroma.Object
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.Endpoint
import Pleroma.Factory
import ExUnit.CaptureLog
import Tesla.Mock
import Mock
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
@@ -66,9 +65,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
- assert results["hashtags"] == [
- %{"name" => "private", "url" => "#{Endpoint.url()}/tag/private"}
- ]
+ assert results["hashtags"] == []
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
@@ -77,9 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
get(conn, "/api/v2/search?q=天子")
|> json_response_and_validate_schema(200)
- assert results["hashtags"] == [
- %{"name" => "天子", "url" => "#{Endpoint.url()}/tag/天子"}
- ]
+ assert results["hashtags"] == []
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
@@ -130,84 +125,97 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
assert [] = results["statuses"]
end
- test "constructs hashtags from search query", %{conn: conn} do
+ test "returns empty results when no hashtags match", %{conn: conn} do
results =
conn
- |> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}")
+ |> get("/api/v2/search?#{URI.encode_query(%{q: "nonexistent"})}")
|> json_response_and_validate_schema(200)
- assert results["hashtags"] == [
- %{"name" => "explicit", "url" => "#{Endpoint.url()}/tag/explicit"},
- %{"name" => "hashtags", "url" => "#{Endpoint.url()}/tag/hashtags"}
- ]
+ assert results["hashtags"] == []
+ end
+
+ test "searches hashtags by multiple words in query", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, _activity1} = CommonAPI.post(user, %{status: "This is my new #computer"})
+ {:ok, _activity2} = CommonAPI.post(user, %{status: "Check out this #laptop"})
+ {:ok, _activity3} = CommonAPI.post(user, %{status: "My #desktop setup"})
+ {:ok, _activity4} = CommonAPI.post(user, %{status: "New #phone arrived"})
results =
conn
- |> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}")
+ |> get("/api/v2/search?#{URI.encode_query(%{q: "new computer"})}")
|> json_response_and_validate_schema(200)
- assert results["hashtags"] == [
- %{"name" => "john", "url" => "#{Endpoint.url()}/tag/john"},
- %{"name" => "doe", "url" => "#{Endpoint.url()}/tag/doe"},
- %{"name" => "JohnDoe", "url" => "#{Endpoint.url()}/tag/JohnDoe"}
- ]
+ hashtag_names = Enum.map(results["hashtags"], & &1["name"])
+ assert "computer" in hashtag_names
+ refute "laptop" in hashtag_names
+ refute "desktop" in hashtag_names
+ refute "phone" in hashtag_names
results =
conn
- |> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}")
+ |> get("/api/v2/search?#{URI.encode_query(%{q: "computer laptop"})}")
|> json_response_and_validate_schema(200)
- assert results["hashtags"] == [
- %{"name" => "accident", "url" => "#{Endpoint.url()}/tag/accident"},
- %{"name" => "prone", "url" => "#{Endpoint.url()}/tag/prone"},
- %{"name" => "AccidentProne", "url" => "#{Endpoint.url()}/tag/AccidentProne"}
- ]
-
- results =
- conn
- |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}")
- |> json_response_and_validate_schema(200)
-
- assert results["hashtags"] == [
- %{"name" => "shpuld", "url" => "#{Endpoint.url()}/tag/shpuld"}
- ]
-
- results =
- conn
- |> get(
- "/api/v2/search?#{URI.encode_query(%{q: "https://www.washingtonpost.com/sports/2020/06/10/" <> "nascar-ban-display-confederate-flag-all-events-properties/"})}"
- )
- |> json_response_and_validate_schema(200)
-
- assert results["hashtags"] == [
- %{"name" => "nascar", "url" => "#{Endpoint.url()}/tag/nascar"},
- %{"name" => "ban", "url" => "#{Endpoint.url()}/tag/ban"},
- %{"name" => "display", "url" => "#{Endpoint.url()}/tag/display"},
- %{"name" => "confederate", "url" => "#{Endpoint.url()}/tag/confederate"},
- %{"name" => "flag", "url" => "#{Endpoint.url()}/tag/flag"},
- %{"name" => "all", "url" => "#{Endpoint.url()}/tag/all"},
- %{"name" => "events", "url" => "#{Endpoint.url()}/tag/events"},
- %{"name" => "properties", "url" => "#{Endpoint.url()}/tag/properties"},
- %{
- "name" => "NascarBanDisplayConfederateFlagAllEventsProperties",
- "url" =>
- "#{Endpoint.url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties"
- }
- ]
+ hashtag_names = Enum.map(results["hashtags"], & &1["name"])
+ assert "computer" in hashtag_names
+ assert "laptop" in hashtag_names
+ refute "desktop" in hashtag_names
+ refute "phone" in hashtag_names
end
test "supports pagination of hashtags search results", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, _activity1} = CommonAPI.post(user, %{status: "First #alpha hashtag"})
+ {:ok, _activity2} = CommonAPI.post(user, %{status: "Second #beta hashtag"})
+ {:ok, _activity3} = CommonAPI.post(user, %{status: "Third #gamma hashtag"})
+ {:ok, _activity4} = CommonAPI.post(user, %{status: "Fourth #delta hashtag"})
+
results =
conn
- |> get(
- "/api/v2/search?#{URI.encode_query(%{q: "#some #text #with #hashtags", limit: 2, offset: 1})}"
- )
+ |> get("/api/v2/search?#{URI.encode_query(%{q: "a", limit: 2, offset: 1})}")
|> json_response_and_validate_schema(200)
- assert results["hashtags"] == [
- %{"name" => "text", "url" => "#{Endpoint.url()}/tag/text"},
- %{"name" => "with", "url" => "#{Endpoint.url()}/tag/with"}
- ]
+ hashtag_names = Enum.map(results["hashtags"], & &1["name"])
+
+ # Should return 2 hashtags (alpha, beta, gamma, delta all contain 'a')
+ # With offset 1, we skip the first one, so we get 2 of the remaining 3
+ assert length(hashtag_names) == 2
+ assert Enum.all?(hashtag_names, &String.contains?(&1, "a"))
+ end
+
+ test "searches real hashtags from database", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, _activity1} = CommonAPI.post(user, %{status: "Check out this #car"})
+ {:ok, _activity2} = CommonAPI.post(user, %{status: "Fast #racecar on the track"})
+ {:ok, _activity3} = CommonAPI.post(user, %{status: "NASCAR #nascar racing"})
+
+ results =
+ conn
+ |> get("/api/v2/search?#{URI.encode_query(%{q: "car"})}")
+ |> json_response_and_validate_schema(200)
+
+ hashtag_names = Enum.map(results["hashtags"], & &1["name"])
+
+ # Should return car, racecar, and nascar since they all contain "car"
+ assert "car" in hashtag_names
+ assert "racecar" in hashtag_names
+ assert "nascar" in hashtag_names
+
+ # Search for "race" - should return racecar
+ results =
+ conn
+ |> get("/api/v2/search?#{URI.encode_query(%{q: "race"})}")
+ |> json_response_and_validate_schema(200)
+
+ hashtag_names = Enum.map(results["hashtags"], & &1["name"])
+
+ assert "racecar" in hashtag_names
+ refute "car" in hashtag_names
+ refute "nascar" in hashtag_names
end
test "excludes a blocked users from search results", %{conn: conn} do
@@ -314,7 +322,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
- assert results["hashtags"] == ["2hu"]
+ assert results["hashtags"] == []
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
index a4bca6cf9..25a17d5c1 100644
--- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -2483,4 +2483,62 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(:not_found)
end
end
+
+ describe "translating statuses" do
+ setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
+
+ test "it translates a status to user language" do
+ user = insert(:user, language: "fr")
+ %{conn: conn} = oauth_access(["read:statuses"], user: user)
+ another_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(another_user, %{
+ status: "Cześć!",
+ visibility: "public",
+ language: "pl"
+ })
+
+ response =
+ conn
+ |> post("/api/v1/statuses/#{activity.id}/translate")
+ |> json_response_and_validate_schema(200)
+
+ assert response == %{
+ "content" => "!ćśezC",
+ "detected_source_language" => "pl",
+ "provider" => "TranslationMock"
+ }
+ end
+
+ test "it returns an error if no target language provided" do
+ %{conn: conn} = oauth_access(["read:statuses"])
+ another_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(another_user, %{
+ status: "Cześć!",
+ language: "pl"
+ })
+
+ assert conn
+ |> post("/api/v1/statuses/#{activity.id}/translate")
+ |> json_response_and_validate_schema(400)
+ end
+
+ test "it doesn't translate non-public statuses" do
+ %{conn: conn, user: user} = oauth_access(["read:statuses"])
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Cześć!",
+ visibility: "private",
+ language: "pl"
+ })
+
+ assert conn
+ |> post("/api/v1/statuses/#{activity.id}/translate")
+ |> json_response_and_validate_schema(404)
+ end
+ end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/tag_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/tag_controller_test.exs
new file mode 100644
index 000000000..71c8e7fc0
--- /dev/null
+++ b/test/pleroma/web/mastodon_api/controllers/tag_controller_test.exs
@@ -0,0 +1,159 @@
+defmodule Pleroma.Web.MastodonAPI.TagControllerTest do
+ use Pleroma.Web.ConnCase
+
+ import Pleroma.Factory
+ import Tesla.Mock
+
+ alias Pleroma.User
+
+ setup do
+ mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ describe "GET /api/v1/tags/:id" do
+ test "returns 200 with tag" do
+ %{user: user, conn: conn} = oauth_access(["read"])
+
+ tag = insert(:hashtag, name: "jubjub")
+ {:ok, _user} = User.follow_hashtag(user, tag)
+
+ response =
+ conn
+ |> get("/api/v1/tags/jubjub")
+ |> json_response_and_validate_schema(200)
+
+ assert %{
+ "name" => "jubjub",
+ "url" => "http://localhost:4001/tags/jubjub",
+ "history" => [],
+ "following" => true
+ } = response
+ end
+
+ test "returns 404 with unknown tag" do
+ %{conn: conn} = oauth_access(["read"])
+
+ conn
+ |> get("/api/v1/tags/jubjub")
+ |> json_response_and_validate_schema(404)
+ end
+ end
+
+ describe "POST /api/v1/tags/:id/follow" do
+ test "should follow a hashtag" do
+ %{user: user, conn: conn} = oauth_access(["write:follows"])
+ hashtag = insert(:hashtag, name: "jubjub")
+
+ response =
+ conn
+ |> post("/api/v1/tags/jubjub/follow")
+ |> json_response_and_validate_schema(200)
+
+ assert response["following"] == true
+ user = User.get_cached_by_ap_id(user.ap_id)
+ assert User.following_hashtag?(user, hashtag)
+ end
+
+ test "should 404 if hashtag doesn't exist" do
+ %{conn: conn} = oauth_access(["write:follows"])
+
+ response =
+ conn
+ |> post("/api/v1/tags/rubrub/follow")
+ |> json_response_and_validate_schema(404)
+
+ assert response["error"] == "Hashtag not found"
+ end
+ end
+
+ describe "POST /api/v1/tags/:id/unfollow" do
+ test "should unfollow a hashtag" do
+ %{user: user, conn: conn} = oauth_access(["write:follows"])
+ hashtag = insert(:hashtag, name: "jubjub")
+ {:ok, user} = User.follow_hashtag(user, hashtag)
+
+ response =
+ conn
+ |> post("/api/v1/tags/jubjub/unfollow")
+ |> json_response_and_validate_schema(200)
+
+ assert response["following"] == false
+ user = User.get_cached_by_ap_id(user.ap_id)
+ refute User.following_hashtag?(user, hashtag)
+ end
+
+ test "should 404 if hashtag doesn't exist" do
+ %{conn: conn} = oauth_access(["write:follows"])
+
+ response =
+ conn
+ |> post("/api/v1/tags/rubrub/unfollow")
+ |> json_response_and_validate_schema(404)
+
+ assert response["error"] == "Hashtag not found"
+ end
+ end
+
+ describe "GET /api/v1/followed_tags" do
+ test "should list followed tags" do
+ %{user: user, conn: conn} = oauth_access(["read:follows"])
+
+ response =
+ conn
+ |> get("/api/v1/followed_tags")
+ |> json_response_and_validate_schema(200)
+
+ assert Enum.empty?(response)
+
+ hashtag = insert(:hashtag, name: "jubjub")
+ {:ok, _user} = User.follow_hashtag(user, hashtag)
+
+ response =
+ conn
+ |> get("/api/v1/followed_tags")
+ |> json_response_and_validate_schema(200)
+
+ assert [%{"name" => "jubjub"}] = response
+ end
+
+ test "should include a link header to paginate" do
+ %{user: user, conn: conn} = oauth_access(["read:follows"])
+
+ for i <- 1..21 do
+ hashtag = insert(:hashtag, name: "jubjub#{i}}")
+ {:ok, _user} = User.follow_hashtag(user, hashtag)
+ end
+
+ response =
+ conn
+ |> get("/api/v1/followed_tags")
+
+ json = json_response_and_validate_schema(response, 200)
+ assert Enum.count(json) == 20
+ assert [link_header] = get_resp_header(response, "link")
+ assert link_header =~ "rel=\"next\""
+ next_link = extract_next_link_header(link_header)
+
+ response =
+ conn
+ |> get(next_link)
+ |> json_response_and_validate_schema(200)
+
+ assert Enum.count(response) == 1
+ end
+
+ test "should refuse access without read:follows scope" do
+ %{conn: conn} = oauth_access(["write"])
+
+ conn
+ |> get("/api/v1/followed_tags")
+ |> json_response_and_validate_schema(403)
+ end
+ end
+
+ defp extract_next_link_header(header) do
+ [_, next_link] = Regex.run(~r{<(?.*)>; rel="next"}, header)
+ next_link
+ end
+end
diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs
index b1f3523ac..ce5ddd0fc 100644
--- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
import Pleroma.Factory
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index bc6dec32a..e6a164d72 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -951,6 +951,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
assert status.edited_at
end
+ test "it shows post language" do
+ user = insert(:user)
+
+ {:ok, post} = CommonAPI.post(user, %{status: "Szczęść Boże", language: "pl"})
+
+ status = StatusView.render("show.json", activity: post)
+
+ assert status.language == "pl"
+ end
+
+ test "doesn't show post language if it's 'und'" do
+ user = insert(:user)
+
+ {:ok, post} = CommonAPI.post(user, %{status: "sdifjogijodfg", language: "und"})
+
+ status = StatusView.render("show.json", activity: post)
+
+ assert status.language == nil
+ end
+
test "with a source object" do
note =
insert(:note,
diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
index f0c1dd640..f7e52483c 100644
--- a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
+++ b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
@@ -248,8 +248,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
response = get(conn, url)
- assert response.status == 302
- assert redirected_to(response) == media_proxy_url
+ assert response.status == 301
+ assert redirected_to(response, 301) == media_proxy_url
end
test "with `static` param and non-GIF image preview requested, " <>
@@ -290,8 +290,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
response = get(conn, url)
- assert response.status == 302
- assert redirected_to(response) == media_proxy_url
+ assert response.status == 301
+ assert redirected_to(response, 301) == media_proxy_url
end
test "thumbnails PNG images into PNG", %{
@@ -356,5 +356,32 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
assert response.status == 302
assert redirected_to(response) == media_proxy_url
end
+
+ test "redirects to media proxy URI with 301 when image is too small for preview", %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ clear_config([:media_preview_proxy],
+ enabled: true,
+ min_content_length: 1000,
+ image_quality: 85,
+ thumbnail_max_width: 100,
+ thumbnail_max_height: 100
+ )
+
+ Tesla.Mock.mock(fn
+ %{method: :head, url: ^media_proxy_url} ->
+ %Tesla.Env{
+ status: 200,
+ body: "",
+ headers: [{"content-type", "image/png"}, {"content-length", "500"}]
+ }
+ end)
+
+ response = get(conn, url)
+ assert response.status == 301
+ assert redirected_to(response, 301) == media_proxy_url
+ end
end
end
diff --git a/test/pleroma/web/metadata/providers/activity_pub_test.exs b/test/pleroma/web/metadata/providers/activity_pub_test.exs
new file mode 100644
index 000000000..c5cf78a60
--- /dev/null
+++ b/test/pleroma/web/metadata/providers/activity_pub_test.exs
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.ActivityPubTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.Metadata.Providers.ActivityPub
+
+ setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw])
+
+ test "it renders a link for user info" do
+ user = insert(:user)
+ res = ActivityPub.build_tags(%{user: user})
+
+ assert res == [
+ {:link, [rel: "alternate", type: "application/activity+json", href: user.ap_id], []}
+ ]
+ end
+
+ test "it renders a link for a post" do
+ user = insert(:user)
+ {:ok, %{id: activity_id, object: object}} = CommonAPI.post(user, %{status: "hi"})
+
+ result = ActivityPub.build_tags(%{object: object, user: user, activity_id: activity_id})
+
+ assert [
+ {:link,
+ [rel: "alternate", type: "application/activity+json", href: object.data["id"]], []}
+ ] == result
+ end
+
+ test "it returns an empty array for anything else" do
+ result = ActivityPub.build_tags(%{})
+
+ assert result == []
+ end
+end
diff --git a/test/pleroma/web/metadata/providers/feed_test.exs b/test/pleroma/web/metadata/providers/feed_test.exs
index e593453da..40d9d0909 100644
--- a/test/pleroma/web/metadata/providers/feed_test.exs
+++ b/test/pleroma/web/metadata/providers/feed_test.exs
@@ -15,4 +15,10 @@ defmodule Pleroma.Web.Metadata.Providers.FeedTest do
[rel: "alternate", type: "application/atom+xml", href: "/users/lain/feed.atom"], []}
]
end
+
+ test "it doesn't render a link to remote user's feed" do
+ user = insert(:user, nickname: "lain@lain.com", local: false)
+
+ assert Feed.build_tags(%{user: user}) == []
+ end
end
diff --git a/test/pleroma/web/metadata/providers/open_graph_test.exs b/test/pleroma/web/metadata/providers/open_graph_test.exs
index 6a0fc9b10..29cc036ba 100644
--- a/test/pleroma/web/metadata/providers/open_graph_test.exs
+++ b/test/pleroma/web/metadata/providers/open_graph_test.exs
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.Web.Metadata.Providers.OpenGraph
+ alias Pleroma.Web.Metadata.Utils
setup do
ConfigMock
@@ -197,4 +198,58 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do
"http://localhost:4001/proxy/preview/LzAnlke-l5oZbNzWsrHfprX1rGw/aHR0cHM6Ly9wbGVyb21hLmdvdi9hYm91dC9qdWNoZS53ZWJt/juche.webm"
], []} in result
end
+
+ test "meta tag ordering matches attachment order" do
+ user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994")
+
+ note =
+ insert(:note, %{
+ data: %{
+ "actor" => user.ap_id,
+ "tag" => [],
+ "id" => "https://pleroma.gov/objects/whatever",
+ "summary" => "",
+ "content" => "pleroma in a nutshell",
+ "attachment" => [
+ %{
+ "url" => [
+ %{
+ "mediaType" => "image/png",
+ "href" => "https://example.com/first.png",
+ "height" => 1024,
+ "width" => 1280
+ }
+ ]
+ },
+ %{
+ "url" => [
+ %{
+ "mediaType" => "image/png",
+ "href" => "https://example.com/second.png",
+ "height" => 1024,
+ "width" => 1280
+ }
+ ]
+ }
+ ]
+ }
+ })
+
+ result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user})
+
+ assert [
+ {:meta, [property: "og:title", content: Utils.user_name_string(user)], []},
+ {:meta, [property: "og:url", content: "https://pleroma.gov/objects/whatever"], []},
+ {:meta, [property: "og:description", content: "pleroma in a nutshell"], []},
+ {:meta, [property: "og:type", content: "article"], []},
+ {:meta, [property: "og:image", content: "https://example.com/first.png"], []},
+ {:meta, [property: "og:image:alt", content: nil], []},
+ {:meta, [property: "og:image:width", content: "1280"], []},
+ {:meta, [property: "og:image:height", content: "1024"], []},
+ {:meta, [property: "og:image", content: "https://example.com/second.png"], []},
+ {:meta, [property: "og:image:alt", content: nil], []},
+ {:meta, [property: "og:image:width", content: "1280"], []},
+ {:meta, [property: "og:image:height", content: "1024"], []}
+ ] == result
+ end
end
diff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs
index f8d01c5c8..f9e917719 100644
--- a/test/pleroma/web/metadata/providers/twitter_card_test.exs
+++ b/test/pleroma/web/metadata/providers/twitter_card_test.exs
@@ -202,4 +202,58 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do
{:meta, [name: "twitter:player:stream:content_type", content: "video/webm"], []}
] == result
end
+
+ test "meta tag ordering matches attachment order" do
+ user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994")
+
+ note =
+ insert(:note, %{
+ data: %{
+ "actor" => user.ap_id,
+ "tag" => [],
+ "id" => "https://pleroma.gov/objects/whatever",
+ "summary" => "",
+ "content" => "pleroma in a nutshell",
+ "attachment" => [
+ %{
+ "url" => [
+ %{
+ "mediaType" => "image/png",
+ "href" => "https://example.com/first.png",
+ "height" => 1024,
+ "width" => 1280
+ }
+ ]
+ },
+ %{
+ "url" => [
+ %{
+ "mediaType" => "image/png",
+ "href" => "https://example.com/second.png",
+ "height" => 1024,
+ "width" => 1280
+ }
+ ]
+ }
+ ]
+ }
+ })
+
+ result = TwitterCard.build_tags(%{object: note, activity_id: note.data["id"], user: user})
+
+ assert [
+ {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []},
+ {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []},
+ {:meta, [name: "twitter:card", content: "summary_large_image"], []},
+ {:meta, [name: "twitter:image", content: "https://example.com/first.png"], []},
+ {:meta, [name: "twitter:image:alt", content: ""], []},
+ {:meta, [name: "twitter:player:width", content: "1280"], []},
+ {:meta, [name: "twitter:player:height", content: "1024"], []},
+ {:meta, [name: "twitter:card", content: "summary_large_image"], []},
+ {:meta, [name: "twitter:image", content: "https://example.com/second.png"], []},
+ {:meta, [name: "twitter:image:alt", content: ""], []},
+ {:meta, [name: "twitter:player:width", content: "1280"], []},
+ {:meta, [name: "twitter:player:height", content: "1024"], []}
+ ] == result
+ end
end
diff --git a/test/pleroma/web/o_auth/app_test.exs b/test/pleroma/web/o_auth/app_test.exs
index 44219cf90..a69ba371e 100644
--- a/test/pleroma/web/o_auth/app_test.exs
+++ b/test/pleroma/web/o_auth/app_test.exs
@@ -58,16 +58,28 @@ defmodule Pleroma.Web.OAuth.AppTest do
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
{:ok, %App{} = old_app} = App.get_or_make(attrs, ["write"])
+ # backdate the old app so it's within the threshold for being cleaned up
+ one_hour_ago = DateTime.add(DateTime.utc_now(), -3600)
+
+ {:ok, _} =
+ "UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id = $2"
+ |> Pleroma.Repo.query([one_hour_ago, old_app.id])
+
+ # Create the new app after backdating the old one
attrs = %{client_name: "PleromaFE", redirect_uris: "."}
{:ok, %App{} = app} = App.get_or_make(attrs, ["write"])
- # backdate the old app so it's within the threshold for being cleaned up
+ # Ensure the new app has a recent timestamp
+ now = DateTime.utc_now()
+
{:ok, _} =
- "UPDATE apps SET inserted_at = now() - interval '1 hour' WHERE id = #{old_app.id}"
- |> Pleroma.Repo.query()
+ "UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id = $2"
+ |> Pleroma.Repo.query([now, app.id])
App.remove_orphans()
- assert [app] == Pleroma.Repo.all(App)
+ assert [returned_app] = Pleroma.Repo.all(App)
+ assert returned_app.client_name == "PleromaFE"
+ assert returned_app.id == app.id
end
end
diff --git a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs
index 61880e2c0..d152a44cd 100644
--- a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs
@@ -292,10 +292,14 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
User.endorse(user1, user2)
User.endorse(user1, user3)
- [%{"id" => ^id2}, %{"id" => ^id3}] =
+ response =
conn
|> get("/api/v1/pleroma/accounts/#{id1}/endorsements")
|> json_response_and_validate_schema(200)
+
+ assert length(response) == 2
+ assert Enum.any?(response, fn user -> user["id"] == id2 end)
+ assert Enum.any?(response, fn user -> user["id"] == id3 end)
end
test "returns 404 error when specified user is not exist", %{conn: conn} do
diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_download_zip_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_download_zip_test.exs
new file mode 100644
index 000000000..5150a75f0
--- /dev/null
+++ b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_download_zip_test.exs
@@ -0,0 +1,371 @@
+# Pleroma: A lightweight social networking server
+# Copyright © Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerDownloadZipTest do
+ use Pleroma.Web.ConnCase, async: false
+
+ import Tesla.Mock
+ import Pleroma.Factory
+
+ setup_all do
+ # Create a base temp directory for this test module
+ base_temp_dir = Path.join(System.tmp_dir!(), "emoji_test_#{Ecto.UUID.generate()}")
+
+ # Clean up when all tests in module are done
+ on_exit(fn ->
+ File.rm_rf!(base_temp_dir)
+ end)
+
+ {:ok, %{base_temp_dir: base_temp_dir}}
+ end
+
+ setup %{base_temp_dir: base_temp_dir} do
+ # Create a unique subdirectory for each test
+ test_id = Ecto.UUID.generate()
+ temp_dir = Path.join(base_temp_dir, test_id)
+ emoji_dir = Path.join(temp_dir, "emoji")
+
+ # Create the directory structure
+ File.mkdir_p!(emoji_dir)
+
+ # Configure this test to use the temp directory
+ clear_config([:instance, :static_dir], temp_dir)
+
+ admin = insert(:user, is_admin: true)
+ token = insert(:oauth_admin_token, user: admin)
+
+ admin_conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, token)
+
+ Pleroma.Emoji.reload()
+
+ {:ok, %{admin_conn: admin_conn, emoji_path: emoji_dir}}
+ end
+
+ describe "POST /api/pleroma/emoji/packs/download_zip" do
+ setup do
+ clear_config([:instance, :admin_privileges], [:emoji_manage_emoji])
+ end
+
+ test "creates pack from uploaded ZIP file", %{admin_conn: admin_conn, emoji_path: emoji_path} do
+ # Create a test ZIP file with emojis
+ {:ok, zip_path} = create_test_emoji_zip()
+
+ upload = %Plug.Upload{
+ content_type: "application/zip",
+ path: zip_path,
+ filename: "test_pack.zip"
+ }
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: "test_zip_pack",
+ file: upload
+ })
+ |> json_response_and_validate_schema(200) == "ok"
+
+ # Verify pack was created
+ assert File.exists?("#{emoji_path}/test_zip_pack/pack.json")
+ assert File.exists?("#{emoji_path}/test_zip_pack/test_emoji.png")
+
+ # Verify pack.json contents
+ {:ok, pack_json} = File.read("#{emoji_path}/test_zip_pack/pack.json")
+ pack_data = Jason.decode!(pack_json)
+
+ assert pack_data["files"]["test_emoji"] == "test_emoji.png"
+ assert pack_data["pack"]["src_sha256"] != nil
+
+ # Clean up
+ File.rm!(zip_path)
+ end
+
+ test "creates pack from URL", %{admin_conn: admin_conn, emoji_path: emoji_path} do
+ # Mock HTTP request to download ZIP
+ {:ok, zip_path} = create_test_emoji_zip()
+ {:ok, zip_data} = File.read(zip_path)
+
+ mock(fn
+ %{method: :get, url: "https://example.com/emoji_pack.zip"} ->
+ %Tesla.Env{status: 200, body: zip_data}
+ end)
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: "test_zip_pack_url",
+ url: "https://example.com/emoji_pack.zip"
+ })
+ |> json_response_and_validate_schema(200) == "ok"
+
+ # Verify pack was created
+ assert File.exists?("#{emoji_path}/test_zip_pack_url/pack.json")
+ assert File.exists?("#{emoji_path}/test_zip_pack_url/test_emoji.png")
+
+ # Verify pack.json has URL as source
+ {:ok, pack_json} = File.read("#{emoji_path}/test_zip_pack_url/pack.json")
+ pack_data = Jason.decode!(pack_json)
+
+ assert pack_data["pack"]["src"] == "https://example.com/emoji_pack.zip"
+ assert pack_data["pack"]["src_sha256"] != nil
+
+ # Clean up
+ File.rm!(zip_path)
+ end
+
+ test "refuses to overwrite existing pack", %{admin_conn: admin_conn, emoji_path: emoji_path} do
+ # Create existing pack
+ pack_path = Path.join(emoji_path, "test_zip_pack")
+ File.mkdir_p!(pack_path)
+ File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(%{files: %{}}))
+
+ {:ok, zip_path} = create_test_emoji_zip()
+
+ upload = %Plug.Upload{
+ content_type: "application/zip",
+ path: zip_path,
+ filename: "test_pack.zip"
+ }
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: "test_zip_pack",
+ file: upload
+ })
+ |> json_response_and_validate_schema(400) == %{
+ "error" => "Pack already exists, refusing to import test_zip_pack"
+ }
+
+ # Clean up
+ File.rm!(zip_path)
+ end
+
+ test "handles invalid ZIP file", %{admin_conn: admin_conn} do
+ # Create invalid ZIP file
+ invalid_zip_path = Path.join(System.tmp_dir!(), "invalid.zip")
+ File.write!(invalid_zip_path, "not a zip file")
+
+ upload = %Plug.Upload{
+ content_type: "application/zip",
+ path: invalid_zip_path,
+ filename: "invalid.zip"
+ }
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: "test_invalid_pack",
+ file: upload
+ })
+ |> json_response_and_validate_schema(400) == %{
+ "error" => "Could not unzip pack"
+ }
+
+ # Clean up
+ File.rm!(invalid_zip_path)
+ end
+
+ test "handles URL download failure", %{admin_conn: admin_conn} do
+ mock(fn
+ %{method: :get, url: "https://example.com/bad_pack.zip"} ->
+ %Tesla.Env{status: 404, body: "Not found"}
+ end)
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: "test_bad_url_pack",
+ url: "https://example.com/bad_pack.zip"
+ })
+ |> json_response_and_validate_schema(400) == %{
+ "error" => "Could not download pack"
+ }
+ end
+
+ test "requires either file or URL parameter", %{admin_conn: admin_conn} do
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: "test_no_source_pack"
+ })
+ |> json_response_and_validate_schema(400) == %{
+ "error" => "Neither file nor URL was present in the request"
+ }
+ end
+
+ test "returns error when pack name is empty", %{admin_conn: admin_conn} do
+ {:ok, zip_path} = create_test_emoji_zip()
+
+ upload = %Plug.Upload{
+ content_type: "application/zip",
+ path: zip_path,
+ filename: "test_pack.zip"
+ }
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: "",
+ file: upload
+ })
+ |> json_response_and_validate_schema(400) == %{
+ "error" => "Pack name cannot be empty"
+ }
+
+ # Clean up
+ File.rm!(zip_path)
+ end
+
+ test "returns error when unable to create pack directory", %{
+ admin_conn: admin_conn,
+ emoji_path: emoji_path
+ } do
+ # Make the emoji directory read-only to trigger mkdir_p failure
+
+ # Save original permissions
+ {:ok, %{mode: original_mode}} = File.stat(emoji_path)
+
+ # Make emoji directory read-only (no write permission)
+ File.chmod!(emoji_path, 0o555)
+
+ {:ok, zip_path} = create_test_emoji_zip()
+
+ upload = %Plug.Upload{
+ content_type: "application/zip",
+ path: zip_path,
+ filename: "test_pack.zip"
+ }
+
+ # Try to create a pack in the read-only emoji directory
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: "test_readonly_pack",
+ file: upload
+ })
+ |> json_response_and_validate_schema(400) == %{
+ "error" => "Could not create the pack directory"
+ }
+
+ # Clean up - restore original permissions
+ File.chmod!(emoji_path, original_mode)
+ File.rm!(zip_path)
+ end
+
+ test "preserves existing pack.json if present in ZIP", %{
+ admin_conn: admin_conn,
+ emoji_path: emoji_path
+ } do
+ # Create ZIP with pack.json
+ {:ok, zip_path} = create_test_emoji_zip_with_pack_json()
+
+ upload = %Plug.Upload{
+ content_type: "application/zip",
+ path: zip_path,
+ filename: "test_pack_with_json.zip"
+ }
+
+ assert admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: "test_zip_pack_with_json",
+ file: upload
+ })
+ |> json_response_and_validate_schema(200) == "ok"
+
+ # Verify original pack.json was preserved
+ {:ok, pack_json} = File.read("#{emoji_path}/test_zip_pack_with_json/pack.json")
+ pack_data = Jason.decode!(pack_json)
+
+ assert pack_data["pack"]["description"] == "Test pack from ZIP"
+ assert pack_data["pack"]["license"] == "Test License"
+
+ # Clean up
+ File.rm!(zip_path)
+ end
+
+ test "rejects malicious pack names", %{admin_conn: admin_conn} do
+ {:ok, zip_path} = create_test_emoji_zip()
+
+ upload = %Plug.Upload{
+ content_type: "application/zip",
+ path: zip_path,
+ filename: "test_pack.zip"
+ }
+
+ # Test path traversal attempts
+ malicious_names = ["../evil", "../../evil", ".", "..", "evil/../../../etc"]
+
+ Enum.each(malicious_names, fn name ->
+ assert_raise RuntimeError, ~r/Invalid or malicious pack name/, fn ->
+ admin_conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/emoji/packs/download_zip", %{
+ name: name,
+ file: upload
+ })
+ end
+ end)
+
+ # Clean up
+ File.rm!(zip_path)
+ end
+ end
+
+ defp create_test_emoji_zip do
+ tmp_dir = System.tmp_dir!()
+ zip_path = Path.join(tmp_dir, "test_emoji_pack_#{:rand.uniform(10000)}.zip")
+
+ # 1x1 pixel PNG
+ png_data =
+ Base.decode64!(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
+ )
+
+ files = [
+ {~c"test_emoji.png", png_data},
+ # Will be treated as GIF based on extension
+ {~c"another_emoji.gif", png_data}
+ ]
+
+ {:ok, {_name, zip_binary}} = :zip.zip(~c"test_pack.zip", files, [:memory])
+ File.write!(zip_path, zip_binary)
+
+ {:ok, zip_path}
+ end
+
+ defp create_test_emoji_zip_with_pack_json do
+ tmp_dir = System.tmp_dir!()
+ zip_path = Path.join(tmp_dir, "test_emoji_pack_json_#{:rand.uniform(10000)}.zip")
+
+ png_data =
+ Base.decode64!(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
+ )
+
+ pack_json =
+ Jason.encode!(%{
+ pack: %{
+ description: "Test pack from ZIP",
+ license: "Test License"
+ },
+ files: %{
+ "test_emoji" => "test_emoji.png"
+ }
+ })
+
+ files = [
+ {~c"test_emoji.png", png_data},
+ {~c"pack.json", pack_json}
+ ]
+
+ {:ok, {_name, zip_binary}} = :zip.zip(~c"test_pack.zip", files, [:memory])
+ File.write!(zip_path, zip_binary)
+
+ {:ok, zip_path}
+ end
+end
diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
index 8c2dcc1bb..c1e452a1e 100644
--- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
import Pleroma.Factory
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs
index 0d4951a73..702c05504 100644
--- a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs
@@ -7,16 +7,11 @@ defmodule Pleroma.Web.PleromaApi.InstancesControllerTest do
alias Pleroma.Instances
- setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
-
setup do
constant = "http://consistently-unreachable.name/"
- eventual = "http://eventually-unreachable.com/path"
{:ok, %Pleroma.Instances.Instance{unreachable_since: constant_unreachable}} =
- Instances.set_consistently_unreachable(constant)
-
- _eventual_unreachable = Instances.set_unreachable(eventual)
+ Instances.set_unreachable(constant)
%{constant_unreachable: constant_unreachable, constant: constant}
end
diff --git a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs
index be94a02ad..bcc25b83e 100644
--- a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs
@@ -19,10 +19,33 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do
"artist" => "lain",
"album" => "lain radio",
"length" => "180000",
- "externalLink" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1"
+ "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1"
})
- assert %{"title" => "lain radio episode 1"} = json_response_and_validate_schema(conn, 200)
+ assert %{
+ "title" => "lain radio episode 1",
+ "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1"
+ } = json_response_and_validate_schema(conn, 200)
+ end
+
+ test "external_link fallback" do
+ %{conn: conn} = oauth_access(["write"])
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/scrobble", %{
+ "title" => "lain radio episode 2",
+ "artist" => "lain",
+ "album" => "lain radio",
+ "length" => "180000",
+ "externalLink" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2"
+ })
+
+ assert %{
+ "title" => "lain radio episode 2",
+ "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2"
+ } = json_response_and_validate_schema(conn, 200)
end
end
@@ -35,7 +58,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do
title: "lain radio episode 1",
artist: "lain",
album: "lain radio",
- externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1"
+ external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1"
})
{:ok, _activity} =
@@ -43,7 +66,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do
title: "lain radio episode 2",
artist: "lain",
album: "lain radio",
- externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2"
+ external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2"
})
{:ok, _activity} =
@@ -51,7 +74,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do
title: "lain radio episode 3",
artist: "lain",
album: "lain radio",
- externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+3"
+ external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+3"
})
conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles")
diff --git a/test/pleroma/web/plugs/cache_test.exs b/test/pleroma/web/plugs/cache_test.exs
index 0c119528d..09065e94e 100644
--- a/test/pleroma/web/plugs/cache_test.exs
+++ b/test/pleroma/web/plugs/cache_test.exs
@@ -5,7 +5,8 @@
defmodule Pleroma.Web.Plugs.CacheTest do
# Relies on Cachex, has to stay synchronous
use Pleroma.DataCase
- use Plug.Test
+ import Plug.Conn
+ import Plug.Test
alias Pleroma.Web.Plugs.Cache
diff --git a/test/pleroma/web/plugs/digest_plug_test.exs b/test/pleroma/web/plugs/digest_plug_test.exs
index 19f8a6f49..21ccddd88 100644
--- a/test/pleroma/web/plugs/digest_plug_test.exs
+++ b/test/pleroma/web/plugs/digest_plug_test.exs
@@ -4,7 +4,8 @@
defmodule Pleroma.Web.Plugs.DigestPlugTest do
use ExUnit.Case, async: true
- use Plug.Test
+ import Plug.Conn
+ import Plug.Test
test "digest algorithm is taken from digest header" do
body = "{\"hello\": \"world\"}"
diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs
index 6f4d24d9e..a7af3e74e 100644
--- a/test/pleroma/web/plugs/frontend_static_plug_test.exs
+++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
@dir "test/tmp/instance_static"
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
on_exit(fn -> File.rm_rf(@dir) end)
end
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
clear_config([:frontends, :primary], %{"name" => name, "ref" => ref})
path = "#{@dir}/frontends/#{name}/#{ref}"
- File.mkdir_p!(path)
+ Pleroma.Backports.mkdir_p!(path)
File.write!("#{path}/index.html", "from frontend plug")
index = get(conn, "/")
@@ -52,7 +52,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
clear_config([:frontends, :admin], %{"name" => name, "ref" => ref})
path = "#{@dir}/frontends/#{name}/#{ref}"
- File.mkdir_p!(path)
+ Pleroma.Backports.mkdir_p!(path)
File.write!("#{path}/index.html", "from frontend plug")
index = get(conn, "/pleroma/admin/")
@@ -67,7 +67,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
clear_config([:frontends, :primary], %{"name" => name, "ref" => ref})
path = "#{@dir}/frontends/#{name}/#{ref}"
- File.mkdir_p!("#{path}/proxy/rr/ss")
+ Pleroma.Backports.mkdir_p!("#{path}/proxy/rr/ss")
File.write!("#{path}/proxy/rr/ss/Ek7w8WPVcAApOvN.jpg:large", "FB image")
ConfigMock
diff --git a/test/pleroma/web/plugs/idempotency_plug_test.exs b/test/pleroma/web/plugs/idempotency_plug_test.exs
index cc55d341f..3b0131596 100644
--- a/test/pleroma/web/plugs/idempotency_plug_test.exs
+++ b/test/pleroma/web/plugs/idempotency_plug_test.exs
@@ -5,7 +5,8 @@
defmodule Pleroma.Web.Plugs.IdempotencyPlugTest do
# Relies on Cachex, has to stay synchronous
use Pleroma.DataCase
- use Plug.Test
+ import Plug.Conn
+ import Plug.Test
alias Pleroma.Web.Plugs.IdempotencyPlug
alias Plug.Conn
diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs
index f91021a16..b5a5a3334 100644
--- a/test/pleroma/web/plugs/instance_static_test.exs
+++ b/test/pleroma/web/plugs/instance_static_test.exs
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
@dir "test/tmp/instance_static"
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
on_exit(fn -> File.rm_rf(@dir) end)
end
@@ -34,7 +34,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
refute html_response(bundled_index, 200) == "from frontend plug"
path = "#{@dir}/frontends/#{name}/#{ref}"
- File.mkdir_p!(path)
+ Pleroma.Backports.mkdir_p!(path)
File.write!("#{path}/index.html", "from frontend plug")
index = get(conn, "/")
@@ -62,4 +62,79 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
index = get(build_conn(), "/static/kaniini.html")
assert html_response(index, 200) == "rabbit hugs as a service "
end
+
+ test "does not sanitize dangerous files in general, as there can be html and javascript files legitimately in this folder" do
+ # Create a file with a potentially dangerous extension (.json)
+ # This mimics an attacker trying to serve ActivityPub JSON with a static file
+ File.mkdir!(@dir <> "/static")
+ File.write!(@dir <> "/static/malicious.json", "{\"type\": \"ActivityPub\"}")
+
+ conn = get(build_conn(), "/static/malicious.json")
+
+ assert conn.status == 200
+
+ content_type =
+ Enum.find_value(conn.resp_headers, fn
+ {"content-type", value} -> value
+ _ -> nil
+ end)
+
+ assert content_type == "application/json"
+
+ File.write!(@dir <> "/static/safe.jpg", "fake image data")
+
+ conn = get(build_conn(), "/static/safe.jpg")
+
+ assert conn.status == 200
+
+ # Get the content-type
+ content_type =
+ Enum.find_value(conn.resp_headers, fn
+ {"content-type", value} -> value
+ _ -> nil
+ end)
+
+ assert content_type == "image/jpeg"
+ end
+
+ test "always sanitizes emojis to images" do
+ File.mkdir!(@dir <> "/emoji")
+ File.write!(@dir <> "/emoji/malicious.html", "")
+
+ # Request the malicious file
+ conn = get(build_conn(), "/emoji/malicious.html")
+
+ # Verify the file was served (status 200)
+ assert conn.status == 200
+
+ # The content should be served, but with a sanitized content-type
+ content_type =
+ Enum.find_value(conn.resp_headers, fn
+ {"content-type", value} -> value
+ _ -> nil
+ end)
+
+ # It should have been sanitized to application/octet-stream because "application"
+ # is not in the allowed_mime_types list
+ assert content_type == "application/octet-stream"
+
+ # Create a file with an allowed extension (.jpg)
+ File.write!(@dir <> "/emoji/safe.jpg", "fake image data")
+
+ # Request the safe file
+ conn = get(build_conn(), "/emoji/safe.jpg")
+
+ # Verify the file was served (status 200)
+ assert conn.status == 200
+
+ # Get the content-type
+ content_type =
+ Enum.find_value(conn.resp_headers, fn
+ {"content-type", value} -> value
+ _ -> nil
+ end)
+
+ # It should be preserved because "image" is in the allowed_mime_types list
+ assert content_type == "image/jpeg"
+ end
end
diff --git a/test/pleroma/web/plugs/remote_ip_test.exs b/test/pleroma/web/plugs/remote_ip_test.exs
index aea0940f4..37b751370 100644
--- a/test/pleroma/web/plugs/remote_ip_test.exs
+++ b/test/pleroma/web/plugs/remote_ip_test.exs
@@ -4,7 +4,8 @@
defmodule Pleroma.Web.Plugs.RemoteIpTest do
use ExUnit.Case
- use Plug.Test
+ import Plug.Conn
+ import Plug.Test
alias Pleroma.Web.Plugs.RemoteIp
diff --git a/test/pleroma/web/plugs/set_format_plug_test.exs b/test/pleroma/web/plugs/set_format_plug_test.exs
index 4d64fdde6..08a2b02a2 100644
--- a/test/pleroma/web/plugs/set_format_plug_test.exs
+++ b/test/pleroma/web/plugs/set_format_plug_test.exs
@@ -4,7 +4,8 @@
defmodule Pleroma.Web.Plugs.SetFormatPlugTest do
use ExUnit.Case, async: true
- use Plug.Test
+ import Plug.Conn
+ import Plug.Test
alias Pleroma.Web.Plugs.SetFormatPlug
diff --git a/test/pleroma/web/plugs/set_locale_plug_test.exs b/test/pleroma/web/plugs/set_locale_plug_test.exs
index 4f664f84e..a7691ea11 100644
--- a/test/pleroma/web/plugs/set_locale_plug_test.exs
+++ b/test/pleroma/web/plugs/set_locale_plug_test.exs
@@ -4,7 +4,7 @@
defmodule Pleroma.Web.Plugs.SetLocalePlugTest do
use ExUnit.Case, async: true
- use Plug.Test
+ import Plug.Test
alias Pleroma.Web.Plugs.SetLocalePlug
alias Plug.Conn
diff --git a/test/pleroma/web/plugs/uploaded_media_test.exs b/test/pleroma/web/plugs/uploaded_media_test.exs
new file mode 100644
index 000000000..69affa019
--- /dev/null
+++ b/test/pleroma/web/plugs/uploaded_media_test.exs
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.UploadedMediaTest do
+ use ExUnit.Case, async: true
+
+ alias Pleroma.Web.Plugs.Utils
+
+ describe "content-type sanitization with Utils.get_safe_mime_type/2" do
+ test "it allows safe MIME types" do
+ opts = %{allowed_mime_types: ["image", "audio", "video"]}
+
+ assert Utils.get_safe_mime_type(opts, "image/jpeg") == "image/jpeg"
+ assert Utils.get_safe_mime_type(opts, "audio/mpeg") == "audio/mpeg"
+ assert Utils.get_safe_mime_type(opts, "video/mp4") == "video/mp4"
+ end
+
+ test "it sanitizes potentially dangerous content-types" do
+ opts = %{allowed_mime_types: ["image", "audio", "video"]}
+
+ assert Utils.get_safe_mime_type(opts, "application/activity+json") ==
+ "application/octet-stream"
+
+ assert Utils.get_safe_mime_type(opts, "text/html") == "application/octet-stream"
+
+ assert Utils.get_safe_mime_type(opts, "application/javascript") ==
+ "application/octet-stream"
+ end
+
+ test "it sanitizes ActivityPub content types" do
+ opts = %{allowed_mime_types: ["image", "audio", "video"]}
+
+ assert Utils.get_safe_mime_type(opts, "application/activity+json") ==
+ "application/octet-stream"
+
+ assert Utils.get_safe_mime_type(opts, "application/ld+json") == "application/octet-stream"
+ assert Utils.get_safe_mime_type(opts, "application/jrd+json") == "application/octet-stream"
+ end
+
+ test "it sanitizes other potentially dangerous types" do
+ opts = %{allowed_mime_types: ["image", "audio", "video"]}
+
+ assert Utils.get_safe_mime_type(opts, "text/html") == "application/octet-stream"
+
+ assert Utils.get_safe_mime_type(opts, "application/javascript") ==
+ "application/octet-stream"
+
+ assert Utils.get_safe_mime_type(opts, "text/javascript") == "application/octet-stream"
+ assert Utils.get_safe_mime_type(opts, "application/xhtml+xml") == "application/octet-stream"
+ end
+ end
+end
diff --git a/test/pleroma/web/plugs/user_tracking_plug_test.exs b/test/pleroma/web/plugs/user_tracking_plug_test.exs
index 742f04fea..cd9c66448 100644
--- a/test/pleroma/web/plugs/user_tracking_plug_test.exs
+++ b/test/pleroma/web/plugs/user_tracking_plug_test.exs
@@ -21,8 +21,12 @@ defmodule Pleroma.Web.Plugs.UserTrackingPlugTest do
|> assign(:user, user)
|> UserTrackingPlug.call(%{})
- assert user.last_active_at >= test_started_at
- assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+ assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq]
+
+ assert NaiveDateTime.compare(
+ user.last_active_at,
+ NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+ ) in [:lt, :eq]
end
test "doesn't update last_active_at if it was updated recently", %{conn: conn} do
@@ -38,7 +42,7 @@ defmodule Pleroma.Web.Plugs.UserTrackingPlugTest do
|> assign(:user, user)
|> UserTrackingPlug.call(%{})
- assert user.last_active_at == last_active_at
+ assert NaiveDateTime.compare(user.last_active_at, last_active_at) == :eq
end
test "skips updating last_active_at if user ID is nil", %{conn: conn} do
diff --git a/test/pleroma/web/rich_media/card_test.exs b/test/pleroma/web/rich_media/card_test.exs
index 387defc8c..c69f85323 100644
--- a/test/pleroma/web/rich_media/card_test.exs
+++ b/test/pleroma/web/rich_media/card_test.exs
@@ -83,4 +83,23 @@ defmodule Pleroma.Web.RichMedia.CardTest do
Card.get_by_activity(activity)
)
end
+
+ test "refuses to crawl URL in activity from ignored host/domain" do
+ clear_config([:rich_media, :ignore_hosts], ["example.com"])
+
+ user = insert(:user)
+
+ url = "https://example.com/ogp"
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "[test](#{url})",
+ content_type: "text/markdown"
+ })
+
+ refute_enqueued(
+ worker: RichMediaWorker,
+ args: %{"url" => url, "activity_id" => activity.id}
+ )
+ end
end
diff --git a/test/pleroma/web/rich_media/parser_test.exs b/test/pleroma/web/rich_media/parser_test.exs
index 8fd75b57a..1f01d657a 100644
--- a/test/pleroma/web/rich_media/parser_test.exs
+++ b/test/pleroma/web/rich_media/parser_test.exs
@@ -54,7 +54,6 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
{:ok,
%{
"card" => "summary",
- "site" => "@flickr",
"image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg",
"title" => "Small Island Developing States Photo Submission",
"description" => "View the album on Flickr.",
@@ -62,6 +61,13 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
}}
end
+ test "truncates title and description fields" do
+ {:ok, parsed} = Parser.parse("https://instagram.com/longtext")
+
+ assert String.length(parsed["title"]) == 120
+ assert String.length(parsed["description"]) == 200
+ end
+
test "parses OEmbed and filters HTML tags" do
assert Parser.parse("https://example.com/oembed") ==
{:ok,
diff --git a/test/pleroma/web/rich_media/parsers/twitter_card_test.exs b/test/pleroma/web/rich_media/parsers/twitter_card_test.exs
index e84a4e50a..15b272ff2 100644
--- a/test/pleroma/web/rich_media/parsers/twitter_card_test.exs
+++ b/test/pleroma/web/rich_media/parsers/twitter_card_test.exs
@@ -17,10 +17,6 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
assert TwitterCard.parse(html, %{}) ==
%{
- "app:id:googleplay" => "com.nytimes.android",
- "app:name:googleplay" => "NYTimes",
- "app:url:googleplay" => "nytimes://reader/id/100000006583622",
- "site" => nil,
"description" =>
"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
"image" =>
@@ -44,7 +40,7 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
"description" =>
"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
"image" =>
- "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
+ "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg",
"image:alt" => "",
"title" =>
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
@@ -61,16 +57,12 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
assert TwitterCard.parse(html, %{}) ==
%{
- "app:id:googleplay" => "com.nytimes.android",
- "app:name:googleplay" => "NYTimes",
- "app:url:googleplay" => "nytimes://reader/id/100000006583622",
"card" => "summary_large_image",
"description" =>
"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
"image" =>
- "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
+ "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg",
"image:alt" => "",
- "site" => nil,
"title" =>
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
"url" =>
@@ -90,13 +82,11 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
assert TwitterCard.parse(html, %{}) ==
%{
- "site" => "@atlasobscura",
"title" => "The Missing Grave of Margaret Corbin, Revolutionary War Veteran",
"card" => "summary_large_image",
"image" => image_path,
"description" =>
"She's the only woman veteran honored with a monument at West Point. But where was she buried?",
- "site_name" => "Atlas Obscura",
"type" => "article",
"url" => "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point"
}
@@ -109,12 +99,8 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
assert TwitterCard.parse(html, %{}) ==
%{
- "site" => nil,
"title" =>
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
- "app:id:googleplay" => "com.nytimes.android",
- "app:name:googleplay" => "NYTimes",
- "app:url:googleplay" => "nytimes://reader/id/100000006583622",
"description" =>
"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
"image" =>
@@ -124,4 +110,23 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
"https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"
}
end
+
+ test "takes first image if multiple are specified" do
+ html =
+ File.read!("test/fixtures/fulmo.html")
+ |> Floki.parse_document!()
+
+ assert TwitterCard.parse(html, %{}) ==
+ %{
+ "description" => "Pri feoj, kiuj devis ordigi falintan arbon.",
+ "image" => "https://tirifto.xwx.moe/r/ilustrajhoj/pinglordigado.png",
+ "title" => "Fulmo",
+ "type" => "website",
+ "url" => "https://tirifto.xwx.moe/eo/rakontoj/fulmo.html",
+ "image:alt" =>
+ "Meze de arbaro kuŝas falinta trunko, sen pingloj kaj kun branĉoj derompitaj. Post ĝi videblas du feoj: florofeo maldekstre kaj nubofeo dekstre. La florofeo iom kaŝas sin post la trunko. La nubofeo staras kaj tenas amason da pigloj. Ili iom rigardas al si.",
+ "image:height" => "630",
+ "image:width" => "1200"
+ }
+ end
end
diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs
index 262ff11d2..85978e824 100644
--- a/test/pleroma/web/streamer_test.exs
+++ b/test/pleroma/web/streamer_test.exs
@@ -558,6 +558,36 @@ defmodule Pleroma.Web.StreamerTest do
assert_receive {:render_with_user, _, "status_update.json", ^create, _}
refute Streamer.filtered_by_user?(user, edited)
end
+
+ test "it streams posts containing followed hashtags on the 'user' stream", %{
+ user: user,
+ token: oauth_token
+ } do
+ hashtag = insert(:hashtag, %{name: "tenshi"})
+ other_user = insert(:user)
+ {:ok, user} = User.follow_hashtag(user, hashtag)
+
+ Streamer.get_topic_and_add_socket("user", user, oauth_token)
+ {:ok, activity} = CommonAPI.post(other_user, %{status: "hey #tenshi"})
+
+ assert_receive {:render_with_user, _, "update.json", ^activity, _}
+ end
+
+ test "should not stream private posts containing followed hashtags on the 'user' stream", %{
+ user: user,
+ token: oauth_token
+ } do
+ hashtag = insert(:hashtag, %{name: "tenshi"})
+ other_user = insert(:user)
+ {:ok, user} = User.follow_hashtag(user, hashtag)
+
+ Streamer.get_topic_and_add_socket("user", user, oauth_token)
+
+ {:ok, activity} =
+ CommonAPI.post(other_user, %{status: "hey #tenshi", visibility: "private"})
+
+ refute_receive {:render_with_user, _, "update.json", ^activity, _}
+ end
end
describe "public streams" do
diff --git a/test/pleroma/web/twitter_api/controller_test.exs b/test/pleroma/web/twitter_api/controller_test.exs
index 495d371d2..494be9ec7 100644
--- a/test/pleroma/web/twitter_api/controller_test.exs
+++ b/test/pleroma/web/twitter_api/controller_test.exs
@@ -69,7 +69,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
|> hd()
|> Map.keys()
- assert keys -- ["id", "app_name", "valid_until"] == []
+ assert Enum.sort(keys) == Enum.sort(["id", "app_name", "valid_until", "scopes"])
end
test "revoke token", %{token: token} do
diff --git a/test/pleroma/web/web_finger/web_finger_controller_test.exs b/test/pleroma/web/web_finger/web_finger_controller_test.exs
index 80e072163..ef52a4b85 100644
--- a/test/pleroma/web/web_finger/web_finger_controller_test.exs
+++ b/test/pleroma/web/web_finger/web_finger_controller_test.exs
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
use Pleroma.Web.ConnCase
- import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
@@ -34,27 +33,122 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
assert match?(^response_xml, expected_xml)
end
- test "Webfinger JRD" do
+ describe "Webfinger" do
+ test "JRD" do
+ clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world")
+ clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world")
+
+ user =
+ insert(:user,
+ ap_id: "https://hyrule.world/users/zelda"
+ )
+
+ response =
+ build_conn()
+ |> put_req_header("accept", "application/jrd+json")
+ |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@hyrule.world")
+ |> json_response(200)
+
+ assert response["subject"] == "acct:#{user.nickname}@hyrule.world"
+
+ assert response["aliases"] == [
+ "https://hyrule.world/users/zelda"
+ ]
+ end
+
+ test "XML" do
+ clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world")
+ clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world")
+
+ user =
+ insert(:user,
+ ap_id: "https://hyrule.world/users/zelda"
+ )
+
+ response =
+ build_conn()
+ |> put_req_header("accept", "application/xrd+xml")
+ |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost")
+ |> response(200)
+
+ assert response =~ "https://hyrule.world/users/zelda "
+ end
+ end
+
+ test "Webfinger defaults to JSON when no Accept header is provided" do
+ clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world")
+ clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world")
+
user =
insert(:user,
- ap_id: "https://hyrule.world/users/zelda",
- also_known_as: ["https://mushroom.kingdom/users/toad"]
+ ap_id: "https://hyrule.world/users/zelda"
)
response =
build_conn()
- |> put_req_header("accept", "application/jrd+json")
- |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost")
+ |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@hyrule.world")
|> json_response(200)
- assert response["subject"] == "acct:#{user.nickname}@localhost"
+ assert response["subject"] == "acct:#{user.nickname}@hyrule.world"
assert response["aliases"] == [
- "https://hyrule.world/users/zelda",
- "https://mushroom.kingdom/users/toad"
+ "https://hyrule.world/users/zelda"
]
end
+ describe "Webfinger returns also_known_as / aliases in the response" do
+ test "JSON" do
+ clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world")
+ clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world")
+
+ user =
+ insert(:user,
+ ap_id: "https://hyrule.world/users/zelda",
+ also_known_as: [
+ "https://mushroom.kingdom/users/toad",
+ "https://luigi.mansion/users/kingboo"
+ ]
+ )
+
+ response =
+ build_conn()
+ |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@hyrule.world")
+ |> json_response(200)
+
+ assert response["subject"] == "acct:#{user.nickname}@hyrule.world"
+
+ assert response["aliases"] == [
+ "https://hyrule.world/users/zelda",
+ "https://mushroom.kingdom/users/toad",
+ "https://luigi.mansion/users/kingboo"
+ ]
+ end
+
+ test "XML" do
+ clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world")
+ clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world")
+
+ user =
+ insert(:user,
+ ap_id: "https://hyrule.world/users/zelda",
+ also_known_as: [
+ "https://mushroom.kingdom/users/toad",
+ "https://luigi.mansion/users/kingboo"
+ ]
+ )
+
+ response =
+ build_conn()
+ |> put_req_header("accept", "application/xrd+xml")
+ |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost")
+ |> response(200)
+
+ assert response =~ "https://hyrule.world/users/zelda "
+ assert response =~ "https://mushroom.kingdom/users/toad "
+ assert response =~ "https://luigi.mansion/users/kingboo "
+ end
+ end
+
test "reach user on tld, while pleroma is running on subdomain" do
clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com")
@@ -72,17 +166,32 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
assert response["aliases"] == ["https://sub.example.com/users/#{user.nickname}"]
end
- test "it returns 404 when user isn't found (JSON)" do
- result =
- build_conn()
- |> put_req_header("accept", "application/jrd+json")
- |> get("/.well-known/webfinger?resource=acct:jimm@localhost")
- |> json_response(404)
+ describe "it returns 404 when user isn't found" do
+ test "JSON" do
+ result =
+ build_conn()
+ |> put_req_header("accept", "application/jrd+json")
+ |> get("/.well-known/webfinger?resource=acct:jimm@localhost")
+ |> json_response(404)
- assert result == "Couldn't find user"
+ assert result == "Couldn't find user"
+ end
+
+ test "XML" do
+ result =
+ build_conn()
+ |> put_req_header("accept", "application/xrd+xml")
+ |> get("/.well-known/webfinger?resource=acct:jimm@localhost")
+ |> response(404)
+
+ assert result == "Couldn't find user"
+ end
end
- test "Webfinger XML" do
+ test "Returns JSON when format is not supported" do
+ clear_config([Pleroma.Web.Endpoint, :url, :host], "hyrule.world")
+ clear_config([Pleroma.Web.WebFinger, :domain], "hyrule.world")
+
user =
insert(:user,
ap_id: "https://hyrule.world/users/zelda",
@@ -91,34 +200,16 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
response =
build_conn()
- |> put_req_header("accept", "application/xrd+xml")
- |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost")
- |> response(200)
+ |> put_req_header("accept", "text/html")
+ |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@hyrule.world")
+ |> json_response(200)
- assert response =~ "https://hyrule.world/users/zelda "
- assert response =~ "https://mushroom.kingdom/users/toad "
- end
+ assert response["subject"] == "acct:#{user.nickname}@hyrule.world"
- test "it returns 404 when user isn't found (XML)" do
- result =
- build_conn()
- |> put_req_header("accept", "application/xrd+xml")
- |> get("/.well-known/webfinger?resource=acct:jimm@localhost")
- |> response(404)
-
- assert result == "Couldn't find user"
- end
-
- test "Sends a 404 when invalid format" do
- user = insert(:user)
-
- assert capture_log(fn ->
- assert_raise Phoenix.NotAcceptableError, fn ->
- build_conn()
- |> put_req_header("accept", "text/html")
- |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost")
- end
- end) =~ "no supported media type in accept header"
+ assert response["aliases"] == [
+ "https://hyrule.world/users/zelda",
+ "https://mushroom.kingdom/users/toad"
+ ]
end
test "Sends a 400 when resource param is missing" do
diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs
index aefe7b0c2..923074ed5 100644
--- a/test/pleroma/web/web_finger_test.exs
+++ b/test/pleroma/web/web_finger_test.exs
@@ -39,6 +39,23 @@ defmodule Pleroma.Web.WebFingerTest do
end
end
+ test "requires exact match for Endpoint host or WebFinger domain" do
+ clear_config([Pleroma.Web.WebFinger, :domain], "pleroma.dev")
+ user = insert(:user)
+
+ assert {:error, "Couldn't find user"} ==
+ WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}xxxx", "JSON")
+
+ assert {:error, "Couldn't find user"} ==
+ WebFinger.webfinger("#{user.nickname}@pleroma.devxxxx", "JSON")
+
+ assert {:ok, _} =
+ WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "JSON")
+
+ assert {:ok, _} =
+ WebFinger.webfinger("#{user.nickname}@pleroma.dev", "JSON")
+ end
+
describe "fingering" do
test "returns error for nonsensical input" do
assert {:error, _} = WebFinger.finger("bliblablu")
diff --git a/test/pleroma/workers/cron/digest_emails_worker_test.exs b/test/pleroma/workers/cron/digest_emails_worker_test.exs
index e0bdf303e..46be82a4f 100644
--- a/test/pleroma/workers/cron/digest_emails_worker_test.exs
+++ b/test/pleroma/workers/cron/digest_emails_worker_test.exs
@@ -14,7 +14,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do
setup do: clear_config([:email_notifications, :digest])
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/workers/cron/new_users_digest_worker_test.exs b/test/pleroma/workers/cron/new_users_digest_worker_test.exs
index 0e4234cc8..ca4139eac 100644
--- a/test/pleroma/workers/cron/new_users_digest_worker_test.exs
+++ b/test/pleroma/workers/cron/new_users_digest_worker_test.exs
@@ -11,7 +11,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorkerTest do
alias Pleroma.Workers.Cron.NewUsersDigestWorker
setup do
- Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
diff --git a/test/pleroma/workers/delete_worker_test.exs b/test/pleroma/workers/delete_worker_test.exs
new file mode 100644
index 000000000..1becd0c03
--- /dev/null
+++ b/test/pleroma/workers/delete_worker_test.exs
@@ -0,0 +1,39 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.DeleteWorkerTest do
+ use Pleroma.DataCase, async: true
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import Pleroma.Factory
+
+ alias Pleroma.Instances.Instance
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.Workers.DeleteWorker
+
+ describe "instance deletion" do
+ test "creates individual Oban jobs for each user when deleting an instance" do
+ user1 = insert(:user, nickname: "alice@example.com", name: "Alice")
+ user2 = insert(:user, nickname: "bob@example.com", name: "Bob")
+
+ {:ok, job} = Instance.delete("example.com")
+
+ assert_enqueued(
+ worker: DeleteWorker,
+ args: %{"op" => "delete_instance", "host" => "example.com"}
+ )
+
+ {:ok, :ok} = ObanHelpers.perform(job)
+
+ delete_user_jobs = all_enqueued(worker: DeleteWorker, args: %{"op" => "delete_user"})
+
+ assert length(delete_user_jobs) == 2
+
+ user_ids = [user1.id, user2.id]
+ job_user_ids = Enum.map(delete_user_jobs, fn job -> job.args["user_id"] end)
+
+ assert Enum.sort(user_ids) == Enum.sort(job_user_ids)
+ end
+ end
+end
diff --git a/test/pleroma/workers/poll_worker_test.exs b/test/pleroma/workers/poll_worker_test.exs
index 749df8aff..a7cbbdb83 100644
--- a/test/pleroma/workers/poll_worker_test.exs
+++ b/test/pleroma/workers/poll_worker_test.exs
@@ -11,10 +11,10 @@ defmodule Pleroma.Workers.PollWorkerTest do
alias Pleroma.Workers.PollWorker
- test "poll notification job" do
+ test "local poll ending notification job" do
user = insert(:user)
question = insert(:question, user: user)
- activity = insert(:question_activity, question: question)
+ activity = insert(:question_activity, question: question, user: user)
PollWorker.schedule_poll_end(activity)
@@ -44,6 +44,65 @@ defmodule Pleroma.Workers.PollWorkerTest do
# Ensure notifications were streamed out when job executes
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], :_))
assert called(Pleroma.Web.Push.send(:_))
+
+ # Skip refreshing polls for local activities
+ assert activity.local
+
+ refute_enqueued(
+ worker: PollWorker,
+ args: %{"op" => "refresh", "activity_id" => activity.id}
+ )
+ end
+ end
+
+ test "remote poll ending notification job schedules refresh" do
+ user = insert(:user, local: false)
+ question = insert(:question, user: user)
+ activity = insert(:question_activity, question: question, user: user)
+
+ PollWorker.schedule_poll_end(activity)
+
+ expected_job_args = %{"activity_id" => activity.id, "op" => "poll_end"}
+
+ assert_enqueued(args: expected_job_args)
+
+ [job] = all_enqueued(worker: PollWorker)
+ PollWorker.perform(job)
+
+ refute activity.local
+
+ assert_enqueued(
+ worker: PollWorker,
+ args: %{"op" => "refresh", "activity_id" => activity.id}
+ )
+ end
+
+ test "poll refresh" do
+ user = insert(:user, local: false)
+ question = insert(:question, user: user)
+ activity = insert(:question_activity, question: question)
+
+ PollWorker.new(%{"op" => "refresh", "activity_id" => activity.id})
+ |> Oban.insert()
+
+ expected_job_args = %{"activity_id" => activity.id, "op" => "refresh"}
+
+ assert_enqueued(args: expected_job_args)
+
+ with_mocks([
+ {
+ Pleroma.Web.Streamer,
+ [],
+ [
+ stream: fn _, _ -> nil end
+ ]
+ }
+ ]) do
+ [job] = all_enqueued(worker: PollWorker)
+ PollWorker.perform(job)
+
+ # Ensure updates are streamed out
+ assert called(Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], :_))
end
end
end
diff --git a/test/pleroma/workers/publisher_worker_test.exs b/test/pleroma/workers/publisher_worker_test.exs
index 13372bf49..ca432d9bf 100644
--- a/test/pleroma/workers/publisher_worker_test.exs
+++ b/test/pleroma/workers/publisher_worker_test.exs
@@ -7,7 +7,9 @@ defmodule Pleroma.Workers.PublisherWorkerTest do
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
+ import Mock
+ alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
@@ -37,4 +39,85 @@ defmodule Pleroma.Workers.PublisherWorkerTest do
assert {:ok, %Oban.Job{priority: 0}} = Federator.publish(post)
end
end
+
+ describe "Server reachability:" do
+ setup do
+ user = insert(:user)
+ remote_user = insert(:user, local: false, inbox: "https://example.com/inbox")
+ {:ok, _, _} = Pleroma.User.follow(remote_user, user)
+ {:ok, activity} = CommonAPI.post(user, %{status: "Test post"})
+
+ %{
+ user: user,
+ remote_user: remote_user,
+ activity: activity
+ }
+ end
+
+ test "marks server as unreachable only on final failure", %{activity: activity} do
+ with_mock Pleroma.Web.Federator,
+ perform: fn :publish_one, _params -> {:error, :connection_error} end do
+ # First attempt
+ job = %Oban.Job{
+ args: %{
+ "op" => "publish_one",
+ "params" => %{
+ "inbox" => "https://example.com/inbox",
+ "activity_id" => activity.id
+ }
+ },
+ attempt: 1,
+ max_attempts: 5
+ }
+
+ assert {:error, :connection_error} = Pleroma.Workers.PublisherWorker.perform(job)
+ assert Instances.reachable?("https://example.com/inbox")
+
+ # Final attempt
+ job = %{job | attempt: 5}
+ assert {:error, :connection_error} = Pleroma.Workers.PublisherWorker.perform(job)
+ refute Instances.reachable?("https://example.com/inbox")
+ end
+ end
+
+ test "does not mark server as unreachable on successful publish", %{activity: activity} do
+ with_mock Pleroma.Web.Federator,
+ perform: fn :publish_one, _params -> {:ok, %{status: 200}} end do
+ job = %Oban.Job{
+ args: %{
+ "op" => "publish_one",
+ "params" => %{
+ "inbox" => "https://example.com/inbox",
+ "activity_id" => activity.id
+ }
+ },
+ attempt: 1,
+ max_attempts: 5
+ }
+
+ assert :ok = Pleroma.Workers.PublisherWorker.perform(job)
+ assert Instances.reachable?("https://example.com/inbox")
+ end
+ end
+
+ test "cancels job if server is unreachable", %{activity: activity} do
+ # First mark the server as unreachable
+ Instances.set_unreachable("https://example.com/inbox")
+ refute Instances.reachable?("https://example.com/inbox")
+
+ job = %Oban.Job{
+ args: %{
+ "op" => "publish_one",
+ "params" => %{
+ "inbox" => "https://example.com/inbox",
+ "activity_id" => activity.id
+ }
+ },
+ attempt: 1,
+ max_attempts: 5
+ }
+
+ assert {:cancel, :unreachable} = Pleroma.Workers.PublisherWorker.perform(job)
+ end
+ end
end
diff --git a/test/pleroma/workers/reachability_worker_test.exs b/test/pleroma/workers/reachability_worker_test.exs
new file mode 100644
index 000000000..4854aff77
--- /dev/null
+++ b/test/pleroma/workers/reachability_worker_test.exs
@@ -0,0 +1,226 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.ReachabilityWorkerTest do
+ use Pleroma.DataCase, async: true
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import Mock
+
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.Workers.ReachabilityWorker
+
+ setup do
+ ObanHelpers.wipe_all()
+ :ok
+ end
+
+ describe "progressive backoff phases" do
+ test "starts with phase_1min and progresses through phases on failure" do
+ domain = "example.com"
+
+ with_mocks([
+ {Pleroma.HTTP, [], [get: fn _ -> {:error, :timeout} end]},
+ {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]}
+ ]) do
+ # Start with phase_1min
+ job = %Oban.Job{
+ args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 1}
+ }
+
+ # First attempt fails
+ assert {:error, :timeout} = ReachabilityWorker.perform(job)
+
+ # Should schedule retry for phase_1min (attempt 2)
+ retry_jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(retry_jobs) == 1
+ [retry_job] = retry_jobs
+ assert retry_job.args["phase"] == "phase_1min"
+ assert retry_job.args["attempt"] == 2
+
+ # Clear jobs and simulate second attempt failure
+ ObanHelpers.wipe_all()
+
+ retry_job = %Oban.Job{
+ args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 2}
+ }
+
+ assert {:error, :timeout} = ReachabilityWorker.perform(retry_job)
+
+ # Should schedule retry for phase_1min (attempt 3)
+ retry_jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(retry_jobs) == 1
+ [retry_job] = retry_jobs
+ assert retry_job.args["phase"] == "phase_1min"
+ assert retry_job.args["attempt"] == 3
+
+ # Clear jobs and simulate third attempt failure (final attempt for phase_1min)
+ ObanHelpers.wipe_all()
+
+ retry_job = %Oban.Job{
+ args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 3}
+ }
+
+ assert {:error, :timeout} = ReachabilityWorker.perform(retry_job)
+
+ # Should schedule retry for phase_1min (attempt 4)
+ retry_jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(retry_jobs) == 1
+ [retry_job] = retry_jobs
+ assert retry_job.args["phase"] == "phase_1min"
+ assert retry_job.args["attempt"] == 4
+
+ # Clear jobs and simulate fourth attempt failure (final attempt for phase_1min)
+ ObanHelpers.wipe_all()
+
+ retry_job = %Oban.Job{
+ args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 4}
+ }
+
+ assert {:error, :timeout} = ReachabilityWorker.perform(retry_job)
+
+ # Should schedule next phase (phase_15min)
+ next_phase_jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(next_phase_jobs) == 1
+ [next_phase_job] = next_phase_jobs
+ assert next_phase_job.args["phase"] == "phase_15min"
+ assert next_phase_job.args["attempt"] == 1
+ end
+ end
+
+ test "progresses through all phases correctly" do
+ domain = "example.com"
+
+ with_mocks([
+ {Pleroma.HTTP, [], [get: fn _ -> {:error, :timeout} end]},
+ {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]}
+ ]) do
+ # Simulate all phases failing
+ phases = ["phase_1min", "phase_15min", "phase_1hour", "phase_8hour", "phase_24hour"]
+
+ Enum.each(phases, fn phase ->
+ {_interval, max_attempts, next_phase} = get_phase_config(phase)
+
+ # Simulate all attempts failing for this phase
+ Enum.each(1..max_attempts, fn attempt ->
+ job = %Oban.Job{args: %{"domain" => domain, "phase" => phase, "attempt" => attempt}}
+ assert {:error, :timeout} = ReachabilityWorker.perform(job)
+
+ if attempt < max_attempts do
+ # Should schedule retry for same phase
+ retry_jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(retry_jobs) == 1
+ [retry_job] = retry_jobs
+ assert retry_job.args["phase"] == phase
+ assert retry_job.args["attempt"] == attempt + 1
+ ObanHelpers.wipe_all()
+ else
+ # Should schedule next phase (except for final phase)
+ if next_phase != "final" do
+ next_phase_jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(next_phase_jobs) == 1
+ [next_phase_job] = next_phase_jobs
+ assert next_phase_job.args["phase"] == next_phase
+ assert next_phase_job.args["attempt"] == 1
+ ObanHelpers.wipe_all()
+ else
+ # Final phase - no more jobs should be scheduled
+ next_phase_jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(next_phase_jobs) == 0
+ end
+ end
+ end)
+ end)
+ end
+ end
+
+ test "succeeds and stops progression when instance becomes reachable" do
+ domain = "example.com"
+
+ with_mocks([
+ {Pleroma.HTTP, [], [get: fn _ -> {:ok, %{status: 200}} end]},
+ {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]}
+ ]) do
+ job = %Oban.Job{args: %{"domain" => domain, "phase" => "phase_1hour", "attempt" => 2}}
+
+ # Should succeed and not schedule any more jobs
+ assert :ok = ReachabilityWorker.perform(job)
+
+ # Verify set_reachable was called
+ assert_called(Pleroma.Instances.set_reachable("https://#{domain}"))
+
+ # No more jobs should be scheduled
+ next_jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(next_jobs) == 0
+ end
+ end
+
+ test "enforces uniqueness per domain using Oban's conflict detection" do
+ domain = "example.com"
+
+ # Insert first job for the domain
+ job1 =
+ %{
+ "domain" => domain,
+ "phase" => "phase_1min",
+ "attempt" => 1
+ }
+ |> ReachabilityWorker.new()
+ |> Oban.insert()
+
+ assert {:ok, _} = job1
+
+ # Try to insert a second job for the same domain with different phase/attempt
+ job2 =
+ %{
+ "domain" => domain,
+ "phase" => "phase_15min",
+ "attempt" => 1
+ }
+ |> ReachabilityWorker.new()
+ |> Oban.insert()
+
+ # Should fail due to uniqueness constraint (conflict)
+ assert {:ok, %Oban.Job{conflict?: true}} = job2
+
+ # Verify only one job exists for this domain
+ jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(jobs) == 1
+ [existing_job] = jobs
+ assert existing_job.args["domain"] == domain
+ assert existing_job.args["phase"] == "phase_1min"
+ end
+
+ test "handles new jobs with only domain argument and transitions them to the first phase" do
+ domain = "legacy.example.com"
+
+ with_mocks([
+ {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]}
+ ]) do
+ # Create a job with only domain (legacy format)
+ job = %Oban.Job{
+ args: %{"domain" => domain}
+ }
+
+ # Should reschedule with phase_1min and attempt 1
+ assert :ok = ReachabilityWorker.perform(job)
+
+ # Check that a new job was scheduled with the correct format
+ scheduled_jobs = all_enqueued(worker: ReachabilityWorker)
+ assert length(scheduled_jobs) == 1
+ [scheduled_job] = scheduled_jobs
+ assert scheduled_job.args["domain"] == domain
+ assert scheduled_job.args["phase"] == "phase_1min"
+ assert scheduled_job.args["attempt"] == 1
+ end
+ end
+ end
+
+ defp get_phase_config("phase_1min"), do: {1, 4, "phase_15min"}
+ defp get_phase_config("phase_15min"), do: {15, 4, "phase_1hour"}
+ defp get_phase_config("phase_1hour"), do: {60, 4, "phase_8hour"}
+ defp get_phase_config("phase_8hour"), do: {480, 4, "phase_24hour"}
+ defp get_phase_config("phase_24hour"), do: {1440, 4, "final"}
+ defp get_phase_config("final"), do: {nil, 0, nil}
+end
diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs
index 4d53c44ed..7f4789f91 100644
--- a/test/pleroma/workers/receiver_worker_test.exs
+++ b/test/pleroma/workers/receiver_worker_test.exs
@@ -3,13 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.ReceiverWorkerTest do
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: true
use Oban.Testing, repo: Pleroma.Repo
import Mock
import Pleroma.Factory
alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator
alias Pleroma.Workers.ReceiverWorker
@@ -243,4 +244,62 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
assert {:cancel, _} = ReceiverWorker.perform(oban_job)
end
+
+ describe "Server reachability:" do
+ setup do
+ user = insert(:user)
+ remote_user = insert(:user, local: false, ap_id: "https://example.com/users/remote")
+ {:ok, _, _} = Pleroma.User.follow(user, remote_user)
+ {:ok, activity} = CommonAPI.post(remote_user, %{status: "Test post"})
+
+ %{
+ user: user,
+ remote_user: remote_user,
+ activity: activity
+ }
+ end
+
+ test "schedules ReachabilityWorker if host is unreachable", %{activity: activity} do
+ with_mocks [
+ {Pleroma.Web.ActivityPub.Transmogrifier, [],
+ [handle_incoming: fn _ -> {:ok, activity} end]},
+ {Pleroma.Instances, [], [reachable?: fn _ -> false end]},
+ {Pleroma.Web.Federator, [], [perform: fn :incoming_ap_doc, _params -> {:ok, nil} end]}
+ ] do
+ job = %Oban.Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "params" => activity.data
+ }
+ }
+
+ Pleroma.Workers.ReceiverWorker.perform(job)
+
+ assert_enqueued(
+ worker: Pleroma.Workers.ReachabilityWorker,
+ args: %{"domain" => "example.com"}
+ )
+ end
+ end
+
+ test "does not schedule ReachabilityWorker if host is reachable", %{activity: activity} do
+ with_mocks [
+ {Pleroma.Web.ActivityPub.Transmogrifier, [],
+ [handle_incoming: fn _ -> {:ok, activity} end]},
+ {Pleroma.Instances, [], [reachable?: fn _ -> true end]},
+ {Pleroma.Web.Federator, [], [perform: fn :incoming_ap_doc, _params -> {:ok, nil} end]}
+ ] do
+ job = %Oban.Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "params" => activity.data
+ }
+ }
+
+ Pleroma.Workers.ReceiverWorker.perform(job)
+
+ refute_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
+ end
+ end
+ end
end
diff --git a/test/pleroma/workers/remote_fetcher_worker_test.exs b/test/pleroma/workers/remote_fetcher_worker_test.exs
index 9caddb600..6eb6932cb 100644
--- a/test/pleroma/workers/remote_fetcher_worker_test.exs
+++ b/test/pleroma/workers/remote_fetcher_worker_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.RemoteFetcherWorkerTest do
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: true
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Workers.RemoteFetcherWorker
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index 52d4bef1a..304bee5da 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -117,6 +117,8 @@ defmodule Pleroma.DataCase do
Mox.stub_with(Pleroma.ConfigMock, Pleroma.Config)
Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Test.StaticConfig)
Mox.stub_with(Pleroma.StubbedHTTPSignaturesMock, Pleroma.Test.HTTPSignaturesProxy)
+
+ Mox.stub_with(Pleroma.DateTimeMock, Pleroma.DateTime.Impl)
end
def ensure_local_uploader(context) do
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 8f1c6faf9..88c4ed8e5 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -241,6 +241,7 @@ defmodule Pleroma.Factory do
def question_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
+ closed = attrs[:closed] || DateTime.utc_now() |> DateTime.add(86_400) |> DateTime.to_iso8601()
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
@@ -251,7 +252,7 @@ defmodule Pleroma.Factory do
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [user.follower_address],
"context" => Pleroma.Web.ActivityPub.Utils.generate_context_id(),
- "closed" => DateTime.utc_now() |> DateTime.add(86_400) |> DateTime.to_iso8601(),
+ "closed" => closed,
"content" => "Which flavor of ice cream do you prefer?",
"oneOf" => [
%{
@@ -509,7 +510,8 @@ defmodule Pleroma.Factory do
%Pleroma.Activity{
data: data,
actor: data["actor"],
- recipients: data["to"]
+ recipients: data["to"],
+ local: user.local
}
|> Map.merge(attrs)
end
@@ -666,4 +668,11 @@ defmodule Pleroma.Factory do
|> Map.merge(params)
|> Pleroma.Announcement.add_rendered_properties()
end
+
+ def hashtag_factory(params \\ %{}) do
+ %Pleroma.Hashtag{
+ name: "test #{sequence(:hashtag_name, & &1)}"
+ }
+ |> Map.merge(params)
+ end
end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index ed044cf98..f8d11e602 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -955,7 +955,7 @@ defmodule HttpRequestMock do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}}
end
- def get("http://localhost:4001/users/masto_closed/followers", _, _, _) do
+ def get("https://remote.org/users/masto_closed/followers", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
@@ -964,7 +964,7 @@ defmodule HttpRequestMock do
}}
end
- def get("http://localhost:4001/users/masto_closed/followers?page=1", _, _, _) do
+ def get("https://remote.org/users/masto_closed/followers?page=1", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
@@ -973,7 +973,7 @@ defmodule HttpRequestMock do
}}
end
- def get("http://localhost:4001/users/masto_closed/following", _, _, _) do
+ def get("https://remote.org/users/masto_closed/following", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
@@ -982,7 +982,7 @@ defmodule HttpRequestMock do
}}
end
- def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do
+ def get("https://remote.org/users/masto_closed/following?page=1", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
@@ -991,7 +991,7 @@ defmodule HttpRequestMock do
}}
end
- def get("http://localhost:8080/followers/fuser3", _, _, _) do
+ def get("https://remote.org/followers/fuser3", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
@@ -1000,7 +1000,7 @@ defmodule HttpRequestMock do
}}
end
- def get("http://localhost:8080/following/fuser3", _, _, _) do
+ def get("https://remote.org/following/fuser3", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
@@ -1009,7 +1009,7 @@ defmodule HttpRequestMock do
}}
end
- def get("http://localhost:4001/users/fuser2/followers", _, _, _) do
+ def get("https://remote.org/users/fuser2/followers", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
@@ -1018,7 +1018,7 @@ defmodule HttpRequestMock do
}}
end
- def get("http://localhost:4001/users/fuser2/following", _, _, _) do
+ def get("https://remote.org/users/fuser2/following", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
@@ -1494,6 +1494,11 @@ defmodule HttpRequestMock do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}}
end
+ def get("https://instagram.com/longtext", _, _, _) do
+ {:ok,
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/instagram_longtext.html")}}
+ end
+
def get("https://example.com/non-ogp", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}}
@@ -1701,6 +1706,24 @@ defmodule HttpRequestMock do
}}
end
+ def post("https://api-free.deepl.com/v2/translate" <> _, _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/deepl-translation.json"),
+ headers: [{"content-type", "application/json"}]
+ }}
+ end
+
+ def post("https://api-free.deepl.com/v2/languages" <> _, _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/deepl-languages-list.json"),
+ headers: [{"content-type", "application/json"}]
+ }}
+ end
+
def post(url, query, body, headers) do
{:error,
"Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
@@ -1720,7 +1743,8 @@ defmodule HttpRequestMock do
"https://example.com/twitter-card",
"https://google.com/",
"https://pleroma.local/notice/9kCP7V",
- "https://yahoo.com/"
+ "https://yahoo.com/",
+ "https://instagram.com/longtext"
]
def head(url, _query, _body, _headers) when url in @rich_media_mocks do
diff --git a/test/support/mocks.ex b/test/support/mocks.ex
index d84958e15..b26834871 100644
--- a/test/support/mocks.ex
+++ b/test/support/mocks.ex
@@ -33,3 +33,10 @@ Mox.defmock(Pleroma.StubbedHTTPSignaturesMock, for: Pleroma.HTTPSignaturesAPI)
Mox.defmock(Pleroma.LoggerMock, for: Pleroma.Logging)
Mox.defmock(Pleroma.Uploaders.S3.ExAwsMock, for: Pleroma.Uploaders.S3.ExAwsAPI)
+
+Mox.defmock(Pleroma.Language.LanguageDetectorMock,
+ for: Pleroma.Language.LanguageDetector.Provider
+)
+
+Mox.defmock(Pleroma.DateTimeMock, for: Pleroma.DateTime)
+Mox.defmock(Pleroma.MogrifyMock, for: Pleroma.MogrifyBehaviour)
diff --git a/test/support/translation_mock.ex b/test/support/translation_mock.ex
new file mode 100644
index 000000000..84ed8f696
--- /dev/null
+++ b/test/support/translation_mock.ex
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule TranslationMock do
+ alias Pleroma.Language.Translation.Provider
+
+ use Provider
+
+ @behaviour Provider
+
+ @name "TranslationMock"
+
+ @impl Provider
+ def configured?, do: true
+
+ @impl Provider
+ def translate(content, source_language, _target_language) do
+ {:ok,
+ %{
+ content: content |> String.reverse(),
+ detected_source_language: source_language,
+ provider: @name
+ }}
+ end
+
+ @impl Provider
+ def supported_languages(_) do
+ {:ok, ["en", "pl"]}
+ end
+
+ @impl Provider
+ def languages_matrix do
+ {:ok,
+ %{
+ "en" => ["pl"],
+ "pl" => ["en"]
+ }}
+ end
+
+ @impl Provider
+ def name, do: @name
+end
diff --git a/test/test_helper.exs b/test/test_helper.exs
index fed7ce8a7..dc6c05a74 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -2,8 +2,6 @@
# Copyright © 2017-2022 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
-Code.put_compiler_option(:warnings_as_errors, true)
-
ExUnit.configure(capture_log: true, max_cases: System.schedulers_online())
ExUnit.start(exclude: [:federated])
@@ -34,7 +32,13 @@ defmodule Pleroma.Test.StaticConfig do
@behaviour Pleroma.Config.Getting
@config Application.get_all_env(:pleroma)
+ @impl true
def get(path, default \\ nil) do
get_in(@config, path) || default
end
+
+ @impl true
+ def get!(path) do
+ get_in(@config, path)
+ end
end