Commit Graph

531 Commits (be59d31de9d22a56896bf62819d68bbea0ac9731)

Author SHA1 Message Date
Ashley be59d31de9 Rebuild a missing home feed from the read that found it missing
The read path had three ways of answering a request whose window came
back empty, and all three could leave a user staring at a blank timeline.
It returned early whenever the set had any members, so a feed full of
unresolvable ids never got looked at. It only attempted a rebuild for a
request that sent no cursor, so a client holding a cursor got nothing on
every retry. And it marked the rebuild as done before knowing whether it
had worked, in a key nothing ever evicted, so a warm that threw
suppressed the next five minutes of attempts, and the throw itself
escaped the synchronous dispatch and turned the response into a 500.

A missing cached set is now rebuilt in-request behind a non-blocking
30-second lock: a reader that loses the lock serves nothing rather than
running a second full warm, one that wins releases it in a finally, and a
warm that throws is logged and leaves the feed eligible for the next
request. The page is then re-read through the same window helper, so the
cursor the client sent survives the rebuild. The rebuild goes 100 members
deep and queues the deepening job only when it filled that depth.

Three existing cases asserted the old bookkeeping rather than the
behaviour it was meant to protect. They keep their 200s and their empty
bodies and now assert the single-warm guarantee directly.
6 days ago
Ashley ef143a7880 Serve the newest window when a max_id cursor falls below the feed
Every cached timeline is trimmed, so a client that persisted a cursor
from before the last trim sends an id whose score sits under every
remaining member. The strict getter correctly finds nothing older, the
endpoint returned an empty page, and the client kept sending the same
dead cursor on every retry. A separate getRankedMaxIdOrRecent getter
leaves getRankedMaxId's strict contract intact: when the window is empty,
the feed still has members and the cursor resolves below the oldest of
them, the newest window is served instead. A cursor at or above the
oldest member is genuine end-of-feed and still answers with an empty page.

Re-derived on staging on top of the non-positive-cursor helpers
(normaliseHomeCursor/fetchHomeWindow), which it extends.

Verified standalone off staging:
HomeCursorRecoveryTest 6 passed; tests/Feature/Timeline + HomeFeed 77 passed.
7 days ago
Shlee 6e14a116fe
Merge pull request #7864 from pixelfed/fix/timeline-nonpositive-cursor
Treat a non-positive home timeline cursor as no cursor at all
7 days ago
Ashley 1fd8546f78 Treat a non-positive home timeline cursor as no cursor at all
The home window getter was picked by $request->has('min_id'), so a
request carrying min_id=0 (or a blank min_id) alongside a real max_id
paged against a zero floor, short-circuited inside the service and came
back empty every time. Normalising the values alone would not have
helped, because presence was the mechanism, so the has() check is gone
and the cursor is resolved once into an optional pair of positive ids.
min_id still wins over max_id when both are positive.

Verified standalone off staging:
HomeCursorRecoveryTest 8 passed; tests/Feature/Timeline + HomeFeed 79 passed.
7 days ago
Ashley 75d3129868 Floor the network timeline cap so a blank setting cannot empty the feed
INSTANCE_NETWORK_TIMELINE_CACHE_DROPOFF was read straight into the trim
cap and into the warm's query bound. Left blank it casts to 0, so every
insert trimmed the set back to nothing and the network timeline was
permanently empty, while the warm's limit('') silently compiled to no
bound at all. feedLimit() now falls back to the documented default of
100 for any non-numeric or sub-1 value, warmCache normalises its own
limit, and the three call sites stop passing the raw config value.
7 days ago
Daniel Supernault fa72b9b1fa
Fix home timelines 1 week ago
Shlee 4351dad749
Merge pull request #7814 from pixelfed/fix/bookmark-removal-gates
Allow removing bookmarks on ineligible statuses
1 week ago
Shlee 99b59807e2
Merge pull request #7821 from pixelfed/fix/media-sync-license-bool
Use boolean helper for sync_licenses to respect explicit opt-out
1 week ago
Ashley 8308f0dbbe Use boolean helper for sync_licenses to respect explicit opt-out
The sync_licenses flag in accountUpdateCredentials was parsed with a loose
comparison ($request->input('sync_licenses') == true), so any non-empty value
other than "0" counted as opting in. A client sending sync_licenses=false/no/off
to opt out was treated as opting in, triggering MediaSyncLicensePipeline whenever
the default license also changed. Sibling booleans in the same method already use
$request->boolean(); this brings sync_licenses in line.

Added tests/Feature/Api/AccountSyncLicensesBooleanTest.php covering the opt-out
string, the positive control, and the omitted case. Each uses a fresh array cache
so the pf:settings:mls_recently:{uid} rate-limit key does not suppress a dispatch.

Verification (ddev exec "vendor/bin/pest <test> --compact"):
- Before the fix: 1 failed, 2 passed. The sync_licenses='false' case dispatched
  MediaSyncLicensePipeline (assertNotDispatched failed).
- After the fix: 3 passed (6 assertions).
1 week ago
Ashley 6930073035 Match legacy App\Profile morph type in user_filters block and mute queries
Read and delete paths for user_filters now match both the legacy App\Profile and current App\Models\Profile morph strings, so pre-migration block/mute rows are visible and removable again. Write paths keep the canonical class. Adds a migration that deduplicates then normalizes legacy rows.
1 week ago
Ashley 1e44b20709 Allow removing bookmarks on ineligible statuses
Bookmark toggle endpoints validated the status eligibility (no reply/reblog,
allowed scope, media type) before attempting removal, so a bookmark pointing
at a reply, reblog or non-media status could never be deleted — every removal
request 404'd before the delete ran.

Attempt removal first and enforce the create-only gates only when creating a
new bookmark:
- BookmarkController::store() removes an existing bookmark before the gates.
- ApiV1Controller::bookmarkStatus() returns the idempotent bookmarked=true
  response for an already-existing bookmark before the gates.
- ApiV1Controller::unbookmarkStatus() drops the create-only gates; it is a pure
  removal and keeps the private-follower guard.

Response shapes, the private-post handling and BookmarkService usage are
unchanged.

Test: tests/Feature/BookmarkLegacyRemovalTest.php

Verified red -> green with:
  DB_CONNECTION=sqlite DB_DATABASE=:memory: APP_ENV=testing \
  REDIS_HOST=ddev-pixelfed-redis CACHE_STORE=redis \
  vendor/bin/pest tests/Feature/BookmarkLegacyRemovalTest.php

Red (before fix): 2 failed, 2 passed — legacy removal on a reply (web) and a
reblog (api) both returned 404 instead of 200; the two control cases passed.
Green (after fix): 4 passed (13 assertions).
Regression: tests/Feature/Api/StatusTest.php 12 passed (20 assertions).
1 week ago
Shlee a844a763c4
Revert timeline sorted-set feed changes (#7800)
Reverts the timeline rebuild, empty-warm, pagination, feed-recovery, unfollow-preservation and network-reconcile changes (PRs #7796, #7797, #7798), restoring the feed services, controllers and pipelines to their prior state and removing the added timeline tests.

Co-authored-by: Ashley <ashley@asheyhull.net>
1 week ago
Ashley 1bc0acd3e8 Guard repeated empty-timeline warms 1 week ago
Ashley b94031587b Recover timeline feeds past stale members
Prune unresolvable numeric cache members before hydration and walk public
and network windows past a stale block so an eligible post below it is
reached instead of returning an empty page. Home feeds rebuild cold when a
prune empties the cache; public and network feeds advance the recovery
cursor by Redis score so it stands alone on the current sorted-set paging.

Verification: .agents/tasks/pixelfed-timeline-feed-recovery-2026-10-18/verification.md
1 week ago
Daniel Supernault a4d5dfb305
Fix names 1 week ago
Ashley 14d3615893 Derive timeline scroll cursors from display order, not status id
The home and public/network feeds rank by created_at, but the next/prev Link
headers still took the min and max status id on the page. A remote post can
hold a low id with a recent date, so the id-derived cursor pointed at the
wrong row and the next request re-served posts that were already shown,
making infinite scroll stutter or stall.

Build the cursors from the first and last items in display order instead, so
the next page continues from the oldest post actually shown. Add a regression
test that walks the Link header through a feed whose id order and date order
disagree and asserts every post is served exactly once.
1 week ago
Ashley 0c89464d5c Fix static analysis warning and align timeline tests with live-feed fanout
Drop an always-true empty() guard on the home feed Link header that static
analysis flagged as redundant.

Update the fanout and follow-backfill tests to seed a live home feed before
dispatching, since fanout now only appends to feeds that already exist and
skips idle ones. Switch the network null-account test to a real sorted set
instead of a Redis spy so the cached read path actually runs, and force a
deterministic id in the chronological ordering test so it no longer depends
on sub-millisecond insert timing.
1 week ago
Ashley dda3ae7846 Serve the home feed with 200 after a synchronous cold warm
A cold home feed warmed synchronously on the request thread but always
returned 206 with an empty body, so a user whose feed warmed to real
content got nothing until a retry, and a user following only post-less
accounts was stuck on 206 indefinitely. The warm completes before the
response, so read the freshly warmed set and return it with 200, or 200
with an empty array when there is genuinely nothing to show.
2 weeks ago
Ashley d4b51a8d25 Stop treating an empty cursor page as a cold home feed
The home read decided cold-start from the fetched page being empty, so a
min_id poll with nothing new or a max_id past the end forced a warm,
returned 206, and renewed the day-long cold-boot guard on every empty
poll, which could keep a genuinely empty feed from ever warming. Base
cold-start on the set being empty, return 200 for an empty cursor window,
and use a short retry window for the cold-boot guard.
2 weeks ago
Ashley 1cf86d6890 Filter muted, blocked and domain-blocked accounts from the home read
The cached home feed relied entirely on asynchronous removal jobs to
drop muted or blocked accounts, so a backlog or dropped job left their
posts visible until the next rebuild. Filter the hydrated statuses
against the viewer's mutes, blocks and domain blocks at read time,
checking both the status and its reblog author.
2 weeks ago
Ashley 92b3e95843 Keep paginating the home feed when a page is fully filtered
When every status in the fetched window was removed by filtering, the
home timeline returned an empty body with no Link header, so clients
treated it as the end of the feed even though older eligible posts
remained. Emit a next cursor past the fetched window in that case.
2 weeks ago
Ashley 75467a916c Guard local and network warm-on-empty with a lock
The cache_check closure returned null, so Cache::remember treated it as a
miss and ran the empty-set check on every request. When the set was empty
concurrent requests each forced a full warmCache and deleted each other's
work. Only warm when the set is empty, and take a short non-blocking lock
so a single request rebuilds while the rest skip.
2 weeks ago
Ashley 238fa09dfe Honor min_id on the public timeline when max_id is absent
timelinePublic forced min_id to 1 whenever max_id equalled 0, but a
missing max_id is null and null == 0 is true in PHP, so any request
without max_id discarded a supplied min_id and returned the head of the
feed. Only apply the reset when max_id is actually provided as 0.
2 weeks ago
Ashley 71c801d165 Promote cached timeline read/write paths and retire exp gates
Remove the experimental cached_home_timeline/cached_public_timeline
config and the read- and write-side gates across the home, local, and
network timelines, deleting the non-cached query branches so the
Redis-backed path is the only path. Cold-start warming and 206
semantics are preserved.

Also fixes defects found while promoting the paths: unconditional home
feed cache-forget on status update/delete, consistent trim-to-limit
across the three timeline services, network warmCache depth sourced
from cache_dropoff, and the photos_reblogs_only preference now applied
on the home feed. Adds endpoint and service test coverage.
2 weeks ago
Daniel Supernault 93e2eb6d6f
Update ApiV1Controller, fix private statuses 2 weeks ago
Daniel Supernault 5a7f174203
Fix idempotency in statuses method 2 weeks ago
Daniel Supernault 52207c0ab7
Fix account lookup endpoint to properly support local accounts 2 weeks ago
Daniel Supernault 89c38a73df
Fix bookmarks endpoint 2 weeks ago
Daniel Supernault de05bbc9f6
Fix reply visibility in context endpoints 3 weeks ago
Daniel Supernault cf11e38f81
Add pagination to hashtag feed, with signed CursorToken to enforce max pagination limit 3 weeks ago
Daniel Supernault 10bfe4caf4
Update ApiV1Controller, add pe support to post statuses endpoint 3 weeks ago
Daniel Supernault 03f2625410
Improve status deletion pipeline, process local deletion quickly so the status gets removed from feeds regardless of follower count size 4 weeks ago
Daniel Supernault 44f7e2eceb
Update ApiV1Controller, fix bookmarks StatusService 4 weeks ago
Daniel Supernault f42abe8fbf
Fix reblog attribution 1 month ago
Daniel Supernault bfbc54989f
Fix reblog attribution 1 month ago
Your Name 96fc975eb9 Decode HTML entities in display name after purify sanitize 1 month ago
Your Name acc5c930d6 Invalidate place status caches on API publish and location edits 1 month ago
Your Name eb727687c1 Resolve nested source and marker input with dot notation 1 month ago
Daniel Supernault 91f54c0d0f
Fix Direct Message shims. Fixes #7441 1 month ago
Daniel Supernault d436821483
Update StatusService 1 month ago
Daniel Supernault 25d9753d87
Fix reblog account to show account who reblogged it, not who posted it 1 month ago
dansup 545ad821b0
Merge pull request #7419 from lilithmooncohen/fix/reblog-media-attachments
Render boosts for clients that only read the top level
1 month ago
Lily Cohen 24259fa489 Honour photo_reblogs_only in the home timeline 1 month ago
Lily Cohen 92dfdeda11 Render boosts in clients that only read the top level 1 month ago
Daniel Supernault 9c81a75ad3
Refactor Direct Messages, add Group Chat support and proper context threading with Mastodon 1 month ago
Daniel Supernault 823efcaad7
Add FEP-044f: Consent-respecting quote posts 1 month ago
Your Name ab972559ea lint 1 month ago
Your Name 28b11b9b35 Larastan 2 1 month ago
Your Name 47c29fe410 lint 1 month ago
Your Name 33a6c642ea Larastan 1 to 2 1 month ago