From 33dbbe467d1f4652efd448878aea1323ab921aff Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 11 Dec 2023 01:34:46 -0700 Subject: [PATCH] Add Mutual Followers API endpoint --- .../Controllers/Api/ApiV1Dot1Controller.php | 16 + app/Services/FollowerService.php | 389 +++++++++--------- routes/api.php | 14 +- 3 files changed, 221 insertions(+), 198 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index 298deb705..75d0fe984 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -20,6 +20,7 @@ use App\StatusArchived; use App\User; use App\UserSetting; use App\Services\AccountService; +use App\Services\FollowerService; use App\Services\StatusService; use App\Services\ProfileStatusService; use App\Services\LikeService; @@ -897,4 +898,19 @@ class ApiV1Dot1Controller extends Controller return [200]; } + + public function getMutualAccounts(Request $request, $id) + { + abort_if(!$request->user(), 403); + $account = AccountService::get($id, true); + if(!$account || !isset($account['id'])) { return []; } + $res = collect(FollowerService::mutualAccounts($request->user()->profile_id, $id)) + ->map(function($accountId) { + return AccountService::get($accountId, true); + }) + ->filter() + ->take(24) + ->values(); + return $this->json($res); + } } diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index 5525a5da2..8b5eeced9 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -6,222 +6,239 @@ use Illuminate\Support\Facades\Redis; use Cache; use DB; use App\{ - Follower, - Profile, - User + Follower, + Profile, + User }; use App\Jobs\FollowPipeline\FollowServiceWarmCache; class FollowerService { - const CACHE_KEY = 'pf:services:followers:'; - const FOLLOWERS_SYNC_KEY = 'pf:services:followers:sync-followers:'; - const FOLLOWING_SYNC_KEY = 'pf:services:followers:sync-following:'; - const FOLLOWING_KEY = 'pf:services:follow:following:id:'; - const FOLLOWERS_KEY = 'pf:services:follow:followers:id:'; - const FOLLOWERS_LOCAL_KEY = 'pf:services:follow:local-follower-ids:'; + const CACHE_KEY = 'pf:services:followers:'; + const FOLLOWERS_SYNC_KEY = 'pf:services:followers:sync-followers:'; + const FOLLOWING_SYNC_KEY = 'pf:services:followers:sync-following:'; + const FOLLOWING_KEY = 'pf:services:follow:following:id:'; + const FOLLOWERS_KEY = 'pf:services:follow:followers:id:'; + const FOLLOWERS_LOCAL_KEY = 'pf:services:follow:local-follower-ids:'; + const FOLLOWERS_INTER_KEY = 'pf:services:follow:followers:inter:id:'; - public static function add($actor, $target, $refresh = true) - { - $ts = (int) microtime(true); + public static function add($actor, $target, $refresh = true) + { + $ts = (int) microtime(true); if($refresh) { RelationshipService::refresh($actor, $target); } else { - RelationshipService::forget($actor, $target); + RelationshipService::forget($actor, $target); } - Redis::zadd(self::FOLLOWING_KEY . $actor, $ts, $target); - Redis::zadd(self::FOLLOWERS_KEY . $target, $ts, $actor); - Cache::forget('profile:following:' . $actor); - } + 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) - { - Redis::zrem(self::FOLLOWING_KEY . $actor, $target); - Redis::zrem(self::FOLLOWERS_KEY . $target, $actor); - Cache::forget('pf:services:follower:audience:' . $actor); - Cache::forget('pf:services:follower:audience:' . $target); - AccountService::del($actor); - AccountService::del($target); - RelationshipService::refresh($actor, $target); - Cache::forget('profile:following:' . $actor); - } + public static function remove($actor, $target) + { + Redis::zrem(self::FOLLOWING_KEY . $actor, $target); + Redis::zrem(self::FOLLOWERS_KEY . $target, $actor); + Cache::forget('pf:services:follower:audience:' . $actor); + Cache::forget('pf:services:follower:audience:' . $target); + AccountService::del($actor); + AccountService::del($target); + RelationshipService::refresh($actor, $target); + Cache::forget('profile:following:' . $actor); + } - public static function followers($id, $start = 0, $stop = 10) - { - self::cacheSyncCheck($id, 'followers'); - return Redis::zrevrange(self::FOLLOWERS_KEY . $id, $start, $stop); - } + public static function followers($id, $start = 0, $stop = 10) + { + self::cacheSyncCheck($id, 'followers'); + return Redis::zrevrange(self::FOLLOWERS_KEY . $id, $start, $stop); + } - public static function following($id, $start = 0, $stop = 10) - { - self::cacheSyncCheck($id, 'following'); - return Redis::zrevrange(self::FOLLOWING_KEY . $id, $start, $stop); - } + public static function following($id, $start = 0, $stop = 10) + { + self::cacheSyncCheck($id, 'following'); + return Redis::zrevrange(self::FOLLOWING_KEY . $id, $start, $stop); + } - public static function followersPaginate($id, $page = 1, $limit = 10) - { - $start = $page == 1 ? 0 : $page * $limit - $limit; - $end = $start + ($limit - 1); - return self::followers($id, $start, $end); - } + public static function followersPaginate($id, $page = 1, $limit = 10) + { + $start = $page == 1 ? 0 : $page * $limit - $limit; + $end = $start + ($limit - 1); + return self::followers($id, $start, $end); + } - public static function followingPaginate($id, $page = 1, $limit = 10) - { - $start = $page == 1 ? 0 : $page * $limit - $limit; - $end = $start + ($limit - 1); - return self::following($id, $start, $end); - } + public static function followingPaginate($id, $page = 1, $limit = 10) + { + $start = $page == 1 ? 0 : $page * $limit - $limit; + $end = $start + ($limit - 1); + return self::following($id, $start, $end); + } - public static function followerCount($id, $warmCache = true) - { - if($warmCache) { - self::cacheSyncCheck($id, 'followers'); - } - return Redis::zCard(self::FOLLOWERS_KEY . $id); - } + public static function followerCount($id, $warmCache = true) + { + if($warmCache) { + self::cacheSyncCheck($id, 'followers'); + } + return Redis::zCard(self::FOLLOWERS_KEY . $id); + } - public static function followingCount($id, $warmCache = true) - { - if($warmCache) { - self::cacheSyncCheck($id, 'following'); - } - return Redis::zCard(self::FOLLOWING_KEY . $id); - } + public static function followingCount($id, $warmCache = true) + { + if($warmCache) { + self::cacheSyncCheck($id, 'following'); + } + return Redis::zCard(self::FOLLOWING_KEY . $id); + } - public static function follows(string $actor, string $target) - { - if($actor == $target) { - return false; - } + public static function follows(string $actor, string $target) + { + if($actor == $target) { + return false; + } - if(self::followerCount($target, false) && self::followingCount($actor, false)) { - self::cacheSyncCheck($target, 'followers'); - return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor); - } else { - self::cacheSyncCheck($target, 'followers'); - self::cacheSyncCheck($actor, 'following'); - return Follower::whereProfileId($actor)->whereFollowingId($target)->exists(); - } - } + if(self::followerCount($target, false) && self::followingCount($actor, false)) { + self::cacheSyncCheck($target, 'followers'); + return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor); + } else { + self::cacheSyncCheck($target, 'followers'); + self::cacheSyncCheck($actor, 'following'); + return Follower::whereProfileId($actor)->whereFollowingId($target)->exists(); + } + } - public static function cacheSyncCheck($id, $scope = 'followers') - { - if($scope === 'followers') { - if(Cache::get(self::FOLLOWERS_SYNC_KEY . $id) != null) { - return; - } - FollowServiceWarmCache::dispatch($id)->onQueue('low'); - } - if($scope === 'following') { - if(Cache::get(self::FOLLOWING_SYNC_KEY . $id) != null) { - return; - } - FollowServiceWarmCache::dispatch($id)->onQueue('low'); - } - return; - } + public static function cacheSyncCheck($id, $scope = 'followers') + { + if($scope === 'followers') { + if(Cache::get(self::FOLLOWERS_SYNC_KEY . $id) != null) { + return; + } + FollowServiceWarmCache::dispatch($id)->onQueue('low'); + } + if($scope === 'following') { + if(Cache::get(self::FOLLOWING_SYNC_KEY . $id) != null) { + return; + } + FollowServiceWarmCache::dispatch($id)->onQueue('low'); + } + return; + } - public static function audience($profile, $scope = null) - { - return (new self)->getAudienceInboxes($profile, $scope); - } + public static function audience($profile, $scope = null) + { + return (new self)->getAudienceInboxes($profile, $scope); + } - public static function softwareAudience($profile, $software = 'pixelfed') - { - return collect(self::audience($profile)) - ->filter(function($inbox) use($software) { - $domain = parse_url($inbox, PHP_URL_HOST); - if(!$domain) { - return false; - } - return InstanceService::software($domain) === strtolower($software); - }) - ->unique() - ->values() - ->toArray(); - } + public static function softwareAudience($profile, $software = 'pixelfed') + { + return collect(self::audience($profile)) + ->filter(function($inbox) use($software) { + $domain = parse_url($inbox, PHP_URL_HOST); + if(!$domain) { + return false; + } + return InstanceService::software($domain) === strtolower($software); + }) + ->unique() + ->values() + ->toArray(); + } - protected function getAudienceInboxes($pid, $scope = null) - { - $key = 'pf:services:follower:audience:' . $pid; - $domains = Cache::remember($key, 432000, function() use($pid) { - $profile = Profile::whereNull(['status', 'domain'])->find($pid); - if(!$profile) { - return []; - } - return $profile - ->followers() - ->get() - ->map(function($follow) { - return $follow->sharedInbox ?? $follow->inbox_url; - }) - ->filter() - ->unique() - ->values(); - }); + protected function getAudienceInboxes($pid, $scope = null) + { + $key = 'pf:services:follower:audience:' . $pid; + $domains = Cache::remember($key, 432000, function() use($pid) { + $profile = Profile::whereNull(['status', 'domain'])->find($pid); + if(!$profile) { + return []; + } + return $profile + ->followers() + ->get() + ->map(function($follow) { + return $follow->sharedInbox ?? $follow->inbox_url; + }) + ->filter() + ->unique() + ->values(); + }); - if(!$domains || !$domains->count()) { - return []; - } + if(!$domains || !$domains->count()) { + return []; + } - $banned = InstanceService::getBannedDomains(); + $banned = InstanceService::getBannedDomains(); - if(!$banned || count($banned) === 0) { - return $domains->toArray(); - } + if(!$banned || count($banned) === 0) { + return $domains->toArray(); + } - $res = $domains->filter(function($domain) use($banned) { - $parsed = parse_url($domain, PHP_URL_HOST); - return !in_array($parsed, $banned); - }) - ->values() - ->toArray(); + $res = $domains->filter(function($domain) use($banned) { + $parsed = parse_url($domain, PHP_URL_HOST); + return !in_array($parsed, $banned); + }) + ->values() + ->toArray(); - return $res; - } + return $res; + } - public static function mutualCount($pid, $mid) - { - return Cache::remember(self::CACHE_KEY . ':mutualcount:' . $pid . ':' . $mid, 3600, function() use($pid, $mid) { - return DB::table('followers as u') - ->join('followers as s', 'u.following_id', '=', 's.following_id') - ->where('s.profile_id', $mid) - ->where('u.profile_id', $pid) - ->count(); - }); - } + public static function mutualCount($pid, $mid) + { + return Cache::remember(self::CACHE_KEY . ':mutualcount:' . $pid . ':' . $mid, 3600, function() use($pid, $mid) { + return DB::table('followers as u') + ->join('followers as s', 'u.following_id', '=', 's.following_id') + ->where('s.profile_id', $mid) + ->where('u.profile_id', $pid) + ->count(); + }); + } - public static function mutualIds($pid, $mid, $limit = 3) - { - $key = self::CACHE_KEY . ':mutualids:' . $pid . ':' . $mid . ':limit_' . $limit; - return Cache::remember($key, 3600, function() use($pid, $mid, $limit) { - return DB::table('followers as u') - ->join('followers as s', 'u.following_id', '=', 's.following_id') - ->where('s.profile_id', $mid) - ->where('u.profile_id', $pid) - ->limit($limit) - ->pluck('s.following_id') - ->toArray(); - }); - } + public static function mutualIds($pid, $mid, $limit = 3) + { + $key = self::CACHE_KEY . ':mutualids:' . $pid . ':' . $mid . ':limit_' . $limit; + return Cache::remember($key, 3600, function() use($pid, $mid, $limit) { + return DB::table('followers as u') + ->join('followers as s', 'u.following_id', '=', 's.following_id') + ->where('s.profile_id', $mid) + ->where('u.profile_id', $pid) + ->limit($limit) + ->pluck('s.following_id') + ->toArray(); + }); + } - public static function delCache($id) - { - Redis::del(self::CACHE_KEY . $id); - Redis::del(self::FOLLOWING_KEY . $id); - Redis::del(self::FOLLOWERS_KEY . $id); - Cache::forget(self::FOLLOWERS_SYNC_KEY . $id); - Cache::forget(self::FOLLOWING_SYNC_KEY . $id); - } + public static function mutualAccounts($actorId, $profileId) + { + if($actorId == $profileId) { + return []; + } + $actorKey = self::FOLLOWING_KEY . $actorId; + $profileKey = self::FOLLOWERS_KEY . $profileId; + $key = self::FOLLOWERS_INTER_KEY . $actorId . ':' . $profileId; + $res = Redis::zinterstore($key, [$actorKey, $profileKey]); + if($res) { + return Redis::zrange($key, 0, -1); + } else { + return []; + } + } - public static function localFollowerIds($pid, $limit = 0) - { - $key = self::FOLLOWERS_LOCAL_KEY . $pid; - $res = Cache::remember($key, 7200, function() use($pid) { - return DB::table('followers')->whereFollowingId($pid)->whereLocalProfile(true)->pluck('profile_id')->sort(); - }); - return $limit ? - $res->take($limit)->values()->toArray() : - $res->values()->toArray(); - } + public static function delCache($id) + { + Redis::del(self::CACHE_KEY . $id); + Redis::del(self::FOLLOWING_KEY . $id); + Redis::del(self::FOLLOWERS_KEY . $id); + Cache::forget(self::FOLLOWERS_SYNC_KEY . $id); + Cache::forget(self::FOLLOWING_SYNC_KEY . $id); + } + + public static function localFollowerIds($pid, $limit = 0) + { + $key = self::FOLLOWERS_LOCAL_KEY . $pid; + $res = Cache::remember($key, 7200, function() use($pid) { + return DB::table('followers')->whereFollowingId($pid)->whereLocalProfile(true)->pluck('profile_id')->sort(); + }); + return $limit ? + $res->take($limit)->values()->toArray() : + $res->values()->toArray(); + } } diff --git a/routes/api.php b/routes/api.php index f1e5e7bd1..10a4363c2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -111,12 +111,9 @@ Route::group(['prefix' => 'api'], function() use($middleware) { }); Route::group(['prefix' => 'v1.1'], function() use($middleware) { - $reportMiddleware = $middleware; - $reportMiddleware[] = DeprecatedEndpoint::class; - Route::post('report', 'Api\ApiV1Dot1Controller@report')->middleware($reportMiddleware); + Route::post('report', 'Api\ApiV1Dot1Controller@report')->middleware($middleware); Route::group(['prefix' => 'accounts'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('timelines/home', 'Api\ApiV1Controller@timelineHome')->middleware($middleware); Route::delete('avatar', 'Api\ApiV1Dot1Controller@deleteAvatar')->middleware($middleware); Route::get('{id}/posts', 'Api\ApiV1Dot1Controller@accountPosts')->middleware($middleware); @@ -125,10 +122,10 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('two-factor', 'Api\ApiV1Dot1Controller@accountTwoFactor')->middleware($middleware); Route::get('emails-from-pixelfed', 'Api\ApiV1Dot1Controller@accountEmailsFromPixelfed')->middleware($middleware); Route::get('apps-and-applications', 'Api\ApiV1Dot1Controller@accountApps')->middleware($middleware); + Route::get('mutuals/{id}', 'Api\ApiV1Dot1Controller@getMutualAccounts')->middleware($middleware); }); Route::group(['prefix' => 'collections'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('accounts/{id}', 'CollectionController@getUserCollections')->middleware($middleware); Route::get('items/{id}', 'CollectionController@getItems')->middleware($middleware); Route::get('view/{id}', 'CollectionController@getCollection')->middleware($middleware); @@ -139,7 +136,6 @@ Route::group(['prefix' => 'api'], function() use($middleware) { }); Route::group(['prefix' => 'direct'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('thread', 'DirectMessageController@thread')->middleware($middleware); Route::post('thread/send', 'DirectMessageController@create')->middleware($middleware); Route::delete('thread/message', 'DirectMessageController@delete')->middleware($middleware); @@ -151,19 +147,16 @@ Route::group(['prefix' => 'api'], function() use($middleware) { }); Route::group(['prefix' => 'archive'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::post('add/{id}', 'Api\ApiV1Dot1Controller@archive')->middleware($middleware); Route::post('remove/{id}', 'Api\ApiV1Dot1Controller@unarchive')->middleware($middleware); Route::get('list', 'Api\ApiV1Dot1Controller@archivedPosts')->middleware($middleware); }); Route::group(['prefix' => 'places'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('posts/{id}/{slug}', 'Api\ApiV1Dot1Controller@placesById')->middleware($middleware); }); Route::group(['prefix' => 'stories'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('carousel', 'Stories\StoryApiV1Controller@carousel')->middleware($middleware); Route::post('add', 'Stories\StoryApiV1Controller@add')->middleware($middleware); Route::post('publish', 'Stories\StoryApiV1Controller@publish')->middleware($middleware); @@ -173,20 +166,17 @@ Route::group(['prefix' => 'api'], function() use($middleware) { }); Route::group(['prefix' => 'compose'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('search/location', 'ComposeController@searchLocation')->middleware($middleware); Route::get('settings', 'ComposeController@composeSettings')->middleware($middleware); }); Route::group(['prefix' => 'discover'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('accounts/popular', 'Api\ApiV1Controller@discoverAccountsPopular')->middleware($middleware); Route::get('posts/trending', 'DiscoverController@trendingApi')->middleware($middleware); Route::get('posts/hashtags', 'DiscoverController@trendingHashtags')->middleware($middleware); }); Route::group(['prefix' => 'directory'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('listing', 'PixelfedDirectoryController@get'); });