diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f89e17..9fa3a6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Server Directory ([#3762](https://github.com/pixelfed/pixelfed/pull/3762)) - Manually verify email address (php artisan user:verifyemail) ([682f5f0f](https://github.com/pixelfed/pixelfed/commit/682f5f0f)) - Manually generate in-app registration confirmation links (php artisan user:app-magic-link) ([73eb9e36](https://github.com/pixelfed/pixelfed/commit/73eb9e36)) +- Optional home feed caching ([3328b367](https://github.com/pixelfed/pixelfed/commit/3328b367)) ### Updates - Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2)) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 7a0d6376..8b18efb7 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -61,6 +61,7 @@ use App\Jobs\VideoPipeline\{ use App\Services\{ AccountService, + BookmarkService, CollectionService, FollowerService, InstanceService, @@ -768,6 +769,10 @@ class ApiV1Controller extends Controller ->whereFollowingId($target->id) ->delete(); + if(config('instance.timeline.home.cached')) { + Cache::forget('pf:timelines:home:' . $user->profile_id); + } + FollowerService::remove($user->profile_id, $target->id); $target->decrement('followers_count'); @@ -1935,11 +1940,80 @@ class ApiV1Controller extends Controller $limit = $request->input('limit') ?? 20; $pid = $request->user()->profile_id; - $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) { + $following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { $following = Follower::whereProfileId($pid)->pluck('following_id'); return $following->push($pid)->toArray(); }); + if(config('instance.timeline.home.cached') && (!$min && !$max)) { + $ttl = config('instance.timeline.home.cache_ttl'); + $res = Cache::remember( + 'pf:timelines:home:' . $pid, + $ttl, + function() use( + $following, + $limit, + $pid + ) { + return Status::select( + 'id', + 'uri', + 'caption', + 'rendered', + 'profile_id', + 'type', + 'in_reply_to_id', + 'reblog_of_id', + 'is_nsfw', + 'scope', + 'local', + 'reply_count', + 'comments_disabled', + 'place_id', + 'likes_count', + 'reblogs_count', + 'created_at', + 'updated_at' + ) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereIn('profile_id', $following) + ->whereIn('visibility',['public', 'unlisted', 'private']) + ->orderBy('created_at', 'desc') + ->limit($limit) + ->get() + ->map(function($s) { + $status = StatusService::get($s->id, false); + if(!$status) { + return false; + } + return $status; + }) + ->filter(function($s) { + return $s && isset($s['account']['id']); + }) + ->values() + ->toArray(); + }); + + $res = collect($res) + ->map(function($s) use ($pid) { + $status = StatusService::get($s['id'], false); + if(!$status) { + return false; + } + $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $s['id']); + $status['reblogged'] = (bool) ReblogService::get($pid, $s['id']); + return $status; + }) + ->filter(function($s) { + return $s && isset($s['account']['id']); + }) + ->values() + ->take($limit) + ->toArray(); + } + if($min || $max) { $dir = $min ? '>' : '<'; $id = $min ?? $max; diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index 6c1c4cc4..18bb7e0b 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -454,29 +454,19 @@ class PublicApiController extends Controller $user = $request->user(); $key = 'user:last_active_at:id:'.$user->id; - $ttl = now()->addMinutes(20); - Cache::remember($key, $ttl, function() use($user) { + if(Cache::get($key) == null) { $user->last_active_at = now(); $user->save(); - return; - }); + Cache::put($key, true, 43200); + } $pid = $user->profile_id; - $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) { + $following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { $following = Follower::whereProfileId($pid)->pluck('following_id'); return $following->push($pid)->toArray(); }); - if($recentFeed == true) { - $key = 'profile:home-timeline-cursor:'.$user->id; - $ttl = now()->addMinutes(30); - $min = Cache::remember($key, $ttl, function() use($pid) { - $res = StatusView::whereProfileId($pid)->orderByDesc('status_id')->first(); - return $res ? $res->status_id : null; - }); - } - $filtered = $user ? UserFilterService::filters($user->profile_id) : []; $types = ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']; // $types = ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'text']; @@ -496,6 +486,83 @@ class PublicApiController extends Controller array_push($types, 'poll'); } + if(config('instance.timeline.home.cached') && $limit == 6 && (!$min && !$max)) { + $ttl = config('instance.timeline.home.cache_ttl'); + $res = Cache::remember( + 'pf:timelines:home:' . $pid, + $ttl, + function() use( + $types, + $textOnlyReplies, + $following, + $limit, + $filtered, + $user + ) { + return Status::select( + 'id', + 'uri', + 'caption', + 'rendered', + 'profile_id', + 'type', + 'in_reply_to_id', + 'reblog_of_id', + 'is_nsfw', + 'scope', + 'local', + 'reply_count', + 'comments_disabled', + 'place_id', + 'likes_count', + 'reblogs_count', + 'created_at', + 'updated_at' + ) + ->whereIn('type', $types) + ->when(!$textOnlyReplies, function($q, $textOnlyReplies) { + return $q->whereNull('in_reply_to_id'); + }) + ->whereIn('profile_id', $following) + ->whereIn('visibility',['public', 'unlisted', 'private']) + ->orderBy('created_at', 'desc') + ->limit($limit) + ->get() + ->map(function($s) use ($user) { + $status = StatusService::get($s->id, false); + if(!$status) { + return false; + } + return $status; + }) + ->filter(function($s) use($filtered) { + return $s && in_array($s['account']['id'], $filtered) == false; + }) + ->values() + ->toArray(); + }); + + $res = collect($res) + ->map(function($s) use ($user) { + $status = StatusService::get($s['id'], false); + if(!$status) { + return false; + } + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s['id']); + $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s['id']); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s['id']); + return $status; + }) + ->filter(function($s) use($filtered) { + return $s && in_array($s['account']['id'], $filtered) == false; + }) + ->values() + ->take($limit) + ->toArray(); + + return $res; + } + if($min || $max) { $dir = $min ? '>' : '<'; $id = $min ?? $max; diff --git a/app/Observers/FollowerObserver.php b/app/Observers/FollowerObserver.php index fd5f0088..f230bb79 100644 --- a/app/Observers/FollowerObserver.php +++ b/app/Observers/FollowerObserver.php @@ -4,6 +4,7 @@ namespace App\Observers; use App\Follower; use App\Services\FollowerService; +use Cache; class FollowerObserver { @@ -15,6 +16,10 @@ class FollowerObserver */ public function created(Follower $follower) { + if(config('instance.timeline.home.cached')) { + Cache::forget('pf:timelines:home:' . $follower->profile_id); + } + FollowerService::add($follower->profile_id, $follower->following_id); } diff --git a/app/Observers/StatusObserver.php b/app/Observers/StatusObserver.php index 15d24cbc..8d370b38 100644 --- a/app/Observers/StatusObserver.php +++ b/app/Observers/StatusObserver.php @@ -4,6 +4,7 @@ namespace App\Observers; use App\Status; use App\Services\ProfileStatusService; +use Cache; class StatusObserver { @@ -33,6 +34,10 @@ class StatusObserver */ public function updated(Status $status) { + if(config('instance.timeline.home.cached')) { + Cache::forget('pf:timelines:home:' . $status->profile_id); + } + if(in_array($status->scope, ['public', 'unlisted']) && in_array($status->type, ['photo', 'photo:album', 'video'])) { ProfileStatusService::add($status->profile_id, $status->id); } @@ -46,6 +51,10 @@ class StatusObserver */ public function deleted(Status $status) { + if(config('instance.timeline.home.cached')) { + Cache::forget('pf:timelines:home:' . $status->profile_id); + } + ProfileStatusService::delete($status->profile_id, $status->id); } diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index 45781965..aacf7121 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -27,6 +27,7 @@ class FollowerService RelationshipService::refresh($actor, $target); Redis::zadd(self::FOLLOWING_KEY . $actor, $ts, $target); Redis::zadd(self::FOLLOWERS_KEY . $target, $ts, $actor); + Cache::forget('profile:following:' . $actor); } public static function remove($actor, $target) @@ -38,6 +39,7 @@ class FollowerService AccountService::del($actor); AccountService::del($target); RelationshipService::refresh($actor, $target); + Cache::forget('profile:following:' . $actor); } public static function followers($id, $start = 0, $stop = 10) diff --git a/app/Util/Lexer/RestrictedNames.php b/app/Util/Lexer/RestrictedNames.php index bb7103cf..d8a43b00 100644 --- a/app/Util/Lexer/RestrictedNames.php +++ b/app/Util/Lexer/RestrictedNames.php @@ -164,7 +164,10 @@ class RestrictedNames 'explore', 'export', 'exports', + 'external', 'f', + 'fedi', + 'fediverse', 'feed', 'featured', 'font', @@ -175,10 +178,12 @@ class RestrictedNames 'follow-me', 'follow_me', 'g', + 'go', 'gdpr', 'graph', 'ghost', 'ghosts', + 'global', 'group', 'groups', 'h', @@ -217,7 +222,10 @@ class RestrictedNames 'lab', 'labs', 'legal', + 'link', 'live', + 'look', + 'look-back', 'loop', 'loops', 'location', @@ -226,6 +234,8 @@ class RestrictedNames 'logout', 'm', 'media', + 'mini', + 'micro', 'menu', 'music', 'my2020', @@ -239,6 +249,7 @@ class RestrictedNames 'my2028', 'my2029', 'my2030', + 'my', 'n', 'news', 'new', @@ -249,6 +260,8 @@ class RestrictedNames 'newsrooms', 'news-room', 'news-rooms', + 'network', + 'networks', 'o', 'oauth', 'official', @@ -262,6 +275,8 @@ class RestrictedNames 'password', 'portfolio', 'portfolios', + 'pre', + 'post', 'privacy', 'private', 'q', @@ -273,6 +288,7 @@ class RestrictedNames 'register', 'registers', 'review', + 'reviews', 'reset', 'report', 'results', @@ -328,6 +344,8 @@ class RestrictedNames 'www', 'x', 'y', + 'year', + 'year-in-review', 'z', '400', '401', diff --git a/config/instance.php b/config/instance.php index 31adf491..062b3fb4 100644 --- a/config/instance.php +++ b/config/instance.php @@ -23,6 +23,11 @@ return [ 'email' => env('INSTANCE_CONTACT_EMAIL'), 'timeline' => [ + 'home' => [ + 'cached' => env('PF_HOME_TIMELINE_CACHE', false), + 'cache_ttl' => env('PF_HOME_TIMELINE_CACHE_TTL', 900) + ], + 'local' => [ 'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false) ],