Merge remote-tracking branch 'origin/develop' into fork

This commit is contained in:
marcin mikołajczak 2024-09-06 23:21:43 +02:00
commit d1daa9a0c8
20 changed files with 142 additions and 58 deletions

View file

View file

@ -0,0 +1 @@
Resolved edge case where the API can report you are following a user but the relationship is not fully established.

View file

@ -0,0 +1 @@
Adjust more Oban workers to enforce unique job constraints.

View file

@ -0,0 +1 @@
Accept application/activity+json for requests to .well-known/nodeinfo

View file

@ -58,8 +58,12 @@ def refetch_object(%Object{data: %{"id" => id}} = object) do
end
end
@typep fetcher_errors ::
:error | :reject | :allowed_depth | :fetch | :containment | :transmogrifier
# Note: will create a Create activity, which we need internally at the moment.
@spec fetch_object_from_id(String.t(), list()) :: {:ok, Object.t()} | {:error | :reject, any()}
@spec fetch_object_from_id(String.t(), list()) ::
{:ok, Object.t()} | {fetcher_errors(), any()} | Pipeline.errors()
def fetch_object_from_id(id, options \\ []) do
with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},

View file

@ -93,9 +93,6 @@ def schedule_backup(backup) do
else
true ->
{:error, "Backup is missing id. Please insert it into the Repo first."}
e ->
{:error, e}
end
end
@ -122,14 +119,13 @@ def schedule_delete(backup) do
end
defp permitted?(user) do
with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)},
days = Config.get([__MODULE__, :limit_days]),
diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days),
{_, true} <- {:diff, diff > days} do
true
with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)} do
days = Config.get([__MODULE__, :limit_days])
diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
diff > days
else
{:last, nil} -> true
{:diff, false} -> false
end
end
@ -302,9 +298,6 @@ defp write(query, dir, name, fun) do
)
acc
_ ->
acc
end
end)

View file

@ -12,7 +12,7 @@ defmodule Pleroma.User.Import do
require Logger
@spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()}
@spec perform(atom(), User.t(), String.t()) :: :ok | {:error, any()}
def perform(:mute_import, %User{} = user, actor) do
with {:ok, %User{} = muted_user} <- User.get_or_fetch(actor),
{_, false} <- {:existing_mute, User.mutes_user?(user, muted_user)},
@ -49,7 +49,7 @@ def perform(:follow_import, %User{} = user, actor) do
defp handle_error(op, user_id, error) do
Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}")
error
{:error, error}
end
def blocks_import(%User{} = user, [_ | _] = actors) do

View file

@ -22,22 +22,27 @@ defp mrf, do: Config.get([:pipeline, :mrf], MRF)
defp activity_pub, do: Config.get([:pipeline, :activity_pub], ActivityPub)
defp config, do: Config.get([:pipeline, :config], Config)
@spec common_pipeline(map(), keyword()) ::
{:ok, Activity.t() | Object.t(), keyword()} | {:error | :reject, any()}
@type results :: {:ok, Activity.t() | Object.t(), keyword()}
@type errors :: {:error | :reject, any()}
# The Repo.transaction will wrap the result in an {:ok, _}
# and only returns an {:error, _} if the error encountered was related
# to the SQL transaction
@spec common_pipeline(map(), keyword()) :: results() | errors()
def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do
{:ok, {:ok, activity, meta}} ->
side_effects().handle_after_transaction(meta)
{:ok, activity, meta}
{:ok, value} ->
value
{:ok, {:error, _} = error} ->
error
{:ok, {:reject, _} = error} ->
error
{:error, e} ->
{:error, e}
{:reject, e} ->
{:reject, e}
end
end

View file

@ -26,7 +26,7 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants
require Logger
@spec block(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@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
@ -35,7 +35,7 @@ def block(blocked, blocker) do
end
@spec post_chat_message(User.t(), User.t(), String.t(), list()) ::
{:ok, Activity.t()} | {:error, any()}
{:ok, Activity.t()} | Pipeline.errors()
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
:ok <- validate_chat_attachment_attribution(maybe_attachment, user),
@ -58,7 +58,7 @@ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ [])
)} do
{:ok, activity}
else
{:common_pipeline, {:reject, _} = e} -> e
{:common_pipeline, e} -> e
e -> e
end
end
@ -99,7 +99,8 @@ defp validate_chat_content_length(content, _) do
end
end
@spec unblock(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec unblock(User.t(), User.t()) ::
{:ok, Activity.t()} | {:ok, :no_activity} | Pipeline.errors() | {:error, :not_blocking}
def unblock(blocked, blocker) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
@ -120,7 +121,9 @@ def unblock(blocked, blocker) do
end
@spec follow(User.t(), User.t()) ::
{:ok, User.t(), User.t(), Activity.t() | Object.t()} | {:error, :rejected}
{:ok, User.t(), User.t(), Activity.t() | Object.t()}
| {:error, :rejected}
| Pipeline.errors()
def follow(followed, follower) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@ -145,7 +148,7 @@ def unfollow(unfollowed, follower) do
end
end
@spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()}
@spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors()
def accept_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
@ -154,7 +157,7 @@ def accept_follow_request(follower, followed) do
end
end
@spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} | nil
@spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors() | nil
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
@ -163,7 +166,8 @@ def reject_follow_request(follower, followed) do
end
end
@spec delete(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec delete(String.t(), User.t()) ::
{:ok, Activity.t()} | Pipeline.errors() | {:error, :not_found | String.t()}
def delete(activity_id, user) do
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(activity_id, filter: [])},
@ -213,7 +217,7 @@ def delete(activity_id, user) do
end
end
@spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
@spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, :not_found}
def repeat(id, user, params \\ %{}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
object = %Object{} <- Object.normalize(activity, fetch: false),
@ -231,7 +235,7 @@ def repeat(id, user, params \\ %{}) do
end
end
@spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, :not_found | String.t()}
def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
@ -247,7 +251,8 @@ def unrepeat(id, user) do
end
end
@spec favorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec favorite(String.t(), User.t()) ::
{:ok, Activity.t()} | {:ok, :already_liked} | {:error, :not_found | String.t()}
def favorite(id, %User{} = user) do
case favorite_helper(user, id) do
{:ok, _} = res ->
@ -285,7 +290,8 @@ defp favorite_helper(user, id) do
end
end
@spec unfavorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec unfavorite(String.t(), User.t()) ::
{:ok, Activity.t()} | {:error, :not_found | String.t()}
def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
@ -302,7 +308,7 @@ def unfavorite(id, user) do
end
@spec react_with_emoji(String.t(), User.t(), String.t()) ::
{:ok, Activity.t()} | {:error, any()}
{:ok, Activity.t()} | {:error, String.t()}
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity, fetch: false),
@ -316,7 +322,7 @@ def react_with_emoji(id, user, emoji) do
end
@spec unreact_with_emoji(String.t(), User.t(), String.t()) ::
{:ok, Activity.t()} | {:error, any()}
{:ok, Activity.t()} | {:error, String.t()}
def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)},
@ -329,7 +335,7 @@ def unreact_with_emoji(id, user, emoji) do
end
end
@spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | {:error, any()}
@spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | Pipeline.errors()
def vote(%Object{data: %{"type" => "Question"}} = object, %User{} = user, choices) do
with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object),
@ -540,7 +546,7 @@ def post(user, %{status: _} = data) do
end
end
@spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
@spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, nil}
def update(orig_activity, %User{} = user, changes) do
with orig_object <- Object.normalize(orig_activity),
{:ok, new_object} <- make_update_data(user, orig_object, changes),
@ -576,7 +582,7 @@ defp make_update_data(user, orig_object, changes) do
end
end
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def pin(id, %User{} = user) do
with %Activity{} = activity <- create_activity_by_id(id),
true <- activity_belongs_to_actor(activity, user.ap_id),
@ -616,7 +622,7 @@ defp activity_is_public(activity) do
end
end
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def unpin(id, user) do
with %Activity{} = activity <- create_activity_by_id(id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
@ -631,7 +637,7 @@ def unpin(id, user) do
end
end
@spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
@spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, String.t()}
def add_mute(activity, user, params \\ %{}) do
expires_in = Map.get(params, :expires_in, 0)

View file

@ -92,14 +92,13 @@ def render(
User.get_follow_state(reading_user, target)
end
followed_by =
if following_relationships do
case FollowingRelationship.find(following_relationships, target, reading_user) do
%{state: :follow_accept} -> true
_ -> false
end
else
User.following?(target, reading_user)
followed_by = FollowingRelationship.following?(target, reading_user)
following = FollowingRelationship.following?(reading_user, target)
requested =
cond do
following -> false
true -> match?(:follow_pending, follow_state)
end
subscribing =
@ -114,7 +113,7 @@ def render(
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{
id: to_string(target.id),
following: follow_state == :follow_accept,
following: following,
followed_by: followed_by,
blocking:
UserRelationship.exists?(
@ -150,7 +149,7 @@ def render(
),
subscribing: subscribing,
notifying: subscribing,
requested: follow_state == :follow_pending,
requested: requested,
domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs:
not UserRelationship.exists?(

View file

@ -189,7 +189,7 @@ defmodule Pleroma.Web.Router do
end
pipeline :well_known do
plug(:accepts, ["json", "jrd", "jrd+json", "xml", "xrd+xml"])
plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Workers.ReceiverWorker do
alias Pleroma.User
alias Pleroma.Web.Federator
use Oban.Worker, queue: :federator_incoming, max_attempts: 5
use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
@impl true

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Workers.RemoteFetcherWorker do
alias Pleroma.Object.Fetcher
use Oban.Worker, queue: :background
use Oban.Worker, queue: :background, unique: [period: :infinity]
@impl true
def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Workers.RichMediaWorker do
alias Pleroma.Web.RichMedia.Backfill
alias Pleroma.Web.RichMedia.Card
use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: 300]
use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: :infinity]
@impl true
def perform(%Job{args: %{"op" => "expire", "url" => url} = _args}) do

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.UserRefreshWorker do
use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: 300]
use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: :infinity]
alias Pleroma.User

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Workers.WebPusherWorker do
alias Pleroma.Repo
alias Pleroma.Web.Push.Impl
use Oban.Worker, queue: :web_push
use Oban.Worker, queue: :web_push, unique: [period: :infinity]
@impl true
def perform(%Job{args: %{"op" => "web_push", "notification_id" => notification_id}}) do

View file

@ -22,7 +22,7 @@
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
"credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [: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", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"},
"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"},
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"crypt": {:git, "https://github.com/msantos/crypt.git", "f75cd55325e33cbea198fb41fe41871392f8fb76", [ref: "f75cd55325e33cbea198fb41fe41871392f8fb76"]},
"csv": {:hex, :csv, "2.4.1", "50e32749953b6bf9818dbfed81cf1190e38cdf24f95891303108087486c5925e", [:mix], [{:parallel_stream, "~> 1.0.4", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "54508938ac67e27966b10ef49606e3ad5995d665d7fc2688efb3eab1307c9079"},

View file

@ -41,6 +41,10 @@ defmodule Pleroma.HTMLTest do
<span class="h-card"><a class="u-url mention animate-spin">@<span>foo</span></a></span>
"""
@mention_hashtags_sample """
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
"""
describe "StripTags scrubber" do
test "works as expected" do
expected = """
@ -126,6 +130,15 @@ test "filters invalid microformats markup" do
Pleroma.HTML.Scrubber.TwitterText
)
end
test "does allow mention hashtags" do
expected = """
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
"""
assert expected ==
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
end
end
describe "default scrubber" do
@ -189,6 +202,15 @@ test "filters invalid microformats markup" do
Pleroma.HTML.Scrubber.Default
)
end
test "does allow mention hashtags" do
expected = """
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
"""
assert expected ==
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
end
end
describe "extract_first_external_url_from_object" do

View file

@ -428,6 +428,45 @@ test "represent a relationship for the following and followed user" do
test_relationship_rendering(user, other_user, expected)
end
test "relationship does not indicate following if a FollowingRelationship is missing" do
user = insert(:user)
other_user = insert(:user, local: false)
# Create a follow relationship with the real Follow Activity and Accept it
assert {:ok, _, _, _} = CommonAPI.follow(other_user, user)
assert {:ok, _} = CommonAPI.accept_follow_request(user, other_user)
assert %{data: %{"state" => "accept"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, other_user)
# Fetch the relationship and forcibly delete it to simulate
# a Follow Accept that did not complete processing
%{following_relationships: [relationship]} =
Pleroma.UserRelationship.view_relationships_option(user, [other_user])
assert {:ok, _} = Pleroma.Repo.delete(relationship)
assert %{following_relationships: [], user_relationships: []} ==
Pleroma.UserRelationship.view_relationships_option(user, [other_user])
expected =
Map.merge(
@blank_response,
%{
following: false,
followed_by: false,
muting: false,
muting_notifications: false,
subscribing: false,
notifying: false,
showing_reblogs: true,
id: to_string(other_user.id)
}
)
test_relationship_rendering(user, other_user, expected)
end
test "represent a relationship for the blocking and blocked user" do
user = insert(:user)
other_user = insert(:user)

View file

@ -24,6 +24,19 @@ test "GET /.well-known/nodeinfo", %{conn: conn} do
|> get(href)
|> json_response(200)
end)
accept_types = [
"application/activity+json",
"application/json",
"application/jrd+json"
]
for type <- accept_types do
conn
|> put_req_header("accept", type)
|> get("/.well-known/nodeinfo")
|> json_response(200)
end
end
test "nodeinfo shows staff accounts", %{conn: conn} do