diff --git a/CHANGELOG.md b/CHANGELOG.md index 7394c780b..f434955ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79)) - Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c)) - Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271)) +- Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 51243361b..dee5fa4c6 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -71,6 +71,7 @@ use App\Services\{ CollectionService, FollowerService, HashtagService, + HashtagFollowService, HomeTimelineService, InstanceService, LikeService, @@ -3780,6 +3781,7 @@ class ApiV1Controller extends Controller ); HashtagService::follow($pid, $tag->id); + HashtagFollowService::add($tag->id, $pid); return response()->json(FollowedTagResource::make($follows)->toArray($request)); } @@ -3819,6 +3821,7 @@ class ApiV1Controller extends Controller if($follows) { HashtagService::unfollow($pid, $tag->id); + HashtagFollowService::unfollow($tag->id, $pid); $follows->delete(); } diff --git a/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php b/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php index 745907084..5c09d749a 100644 --- a/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php @@ -11,6 +11,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use App\Services\FollowerService; +use App\Services\StatusService; use App\Services\HomeTimelineService; class FeedRemovePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing @@ -66,6 +67,8 @@ class FeedRemovePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { $ids = FollowerService::localFollowerIds($this->pid); + HomeTimelineService::rem($this->pid, $this->sid); + foreach($ids as $id) { HomeTimelineService::rem($id, $this->sid); } diff --git a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php new file mode 100644 index 000000000..354f19798 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php @@ -0,0 +1,97 @@ +hid . ':' . $this->pid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hfp:hashtag:unfollow:{$this->hid}:{$this->pid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($hid, $pid) + { + $this->hid = $hid; + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $hid = $this->hid; + $pid = $this->pid; + + $statusIds = HomeTimelineService::get($pid, 0, -1); + + if(!$statusIds || !count($statusIds)) { + return; + } + + $followingIds = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { + $following = Follower::whereProfileId($pid)->pluck('following_id'); + return $following->push($pid)->toArray(); + }); + + foreach($statusIds as $id) { + $status = StatusService::get($id, false); + if(!$status) { + HomeTimelineService::rem($pid, $id); + continue; + } + if(!in_array($status['account']['id'], $followingIds)) { + HomeTimelineService::rem($pid, $id); + } + } + } +} diff --git a/app/Observers/HashtagFollowObserver.php b/app/Observers/HashtagFollowObserver.php new file mode 100644 index 000000000..822ee0805 --- /dev/null +++ b/app/Observers/HashtagFollowObserver.php @@ -0,0 +1,53 @@ +hashtag_id, $hashtagFollow->profile_id); + } + + /** + * Handle the HashtagFollow "updated" event. + */ + public function updated(HashtagFollow $hashtagFollow): void + { + // + } + + /** + * Handle the HashtagFollow "deleting" event. + */ + public function deleting(HashtagFollow $hashtagFollow): void + { + HashtagFollowService::unfollow($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + HashtagUnfollowPipeline::dispatch($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + } + + /** + * Handle the HashtagFollow "restored" event. + */ + public function restored(HashtagFollow $hashtagFollow): void + { + // + } + + /** + * Handle the HashtagFollow "force deleted" event. + */ + public function forceDeleted(HashtagFollow $hashtagFollow): void + { + HashtagFollowService::unfollow($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + HashtagUnfollowPipeline::dispatch($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + } +} diff --git a/app/Observers/StatusHashtagObserver.php b/app/Observers/StatusHashtagObserver.php index 569120a20..cac223d51 100644 --- a/app/Observers/StatusHashtagObserver.php +++ b/app/Observers/StatusHashtagObserver.php @@ -38,12 +38,12 @@ class StatusHashtagObserver implements ShouldHandleEventsAfterCommit } /** - * Handle the notification "deleting" event. + * Handle the notification "deleted" event. * * @param \App\StatusHashtag $hashtag * @return void */ - public function deleting(StatusHashtag $hashtag) + public function deleted(StatusHashtag $hashtag) { StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id); DB::table('hashtags')->where('id', $hashtag->hashtag_id)->decrement('cached_count'); diff --git a/app/Services/HashtagFollowService.php b/app/Services/HashtagFollowService.php index 8ddc9e6e2..012c8c00a 100644 --- a/app/Services/HashtagFollowService.php +++ b/app/Services/HashtagFollowService.php @@ -11,11 +11,67 @@ use App\HashtagFollow; class HashtagFollowService { const FOLLOW_KEY = 'pf:services:hashtag-follows:v1:'; + const CACHE_KEY = 'pf:services:hfs:byHid:'; + const CACHE_WARMED = 'pf:services:hfs:wc:byHid'; public static function getPidByHid($hid) { - return Cache::remember(self::FOLLOW_KEY . $hid, 86400, function() use($hid) { - return HashtagFollow::whereHashtagId($hid)->pluck('profile_id')->toArray(); - }); + if(!self::isWarm($hid)) { + return self::warmCache($hid); + } + return self::get($hid); + } + + public static function unfollow($hid, $pid) + { + $list = self::getPidByHid($hid); + if($list && count($list)) { + $list = array_values(array_diff($list, [$pid])); + Cache::put(self::FOLLOW_KEY . $hid, $list, 86400); + } + return; + } + + public static function add($hid, $pid) + { + return Redis::zadd(self::CACHE_KEY . $hid, $pid, $pid); + } + + public static function rem($hid, $pid) + { + return Redis::zrem(self::CACHE_KEY . $hid, $pid); + } + + public static function get($hid) + { + return Redis::zrange(self::CACHE_KEY . $hid, 0, -1); + } + + public static function count($hid) + { + return Redis::zcard(self::CACHE_KEY . $hid); + } + + public static function warmCache($hid) + { + foreach(HashtagFollow::whereHashtagId($hid)->lazyById(20, 'id') as $h) { + if($h) { + self::add($h->hashtag_id, $h->profile_id); + } + } + + self::setWarm($hid); + + return self::get($hid); + } + + public static function isWarm($hid) + { + return Redis::zcount($hid, 0, -1) ?? Redis::zscore(self::CACHE_WARMED, $hid) != null; + } + + public static function setWarm($hid) + { + return Redis::zadd(self::CACHE_WARMED, $hid, $hid); } }