From 37abcf389857b3df91a02261a17a507df8b97a40 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 20 Oct 2021 04:31:07 -0600 Subject: [PATCH] Update public timeline api, use cached sorted set and client side block/mute filtering --- app/Http/Controllers/AccountController.php | 32 +++++++ app/Http/Controllers/Api/ApiV1Controller.php | 34 ++++--- app/Http/Controllers/PublicApiController.php | 98 +++++++------------- app/Services/AccountService.php | 20 ++++ app/Services/UserFilterService.php | 10 ++ routes/web.php | 3 + 6 files changed, 115 insertions(+), 82 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 36cdd258a..1103900fa 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -26,6 +26,8 @@ use League\Fractal; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Transformer\Api\Mastodon\v1\AccountTransformer; +use App\Services\AccountService; +use App\Services\UserFilterService; class AccountController extends Controller { @@ -34,6 +36,8 @@ class AccountController extends Controller 'user.block', ]; + const FILTER_LIMIT = 'You cannot block or mute more than 100 accounts'; + public function __construct() { $this->middleware('auth'); @@ -140,6 +144,12 @@ class AccountController extends Controller ]); $user = Auth::user()->profile; + $count = UserFilterService::muteCount($user->id); + abort_if($count >= 100, 422, self::FILTER_LIMIT); + if($count == 0) { + $filterCount = UserFilter::whereUserId($user->id)->count(); + abort_if($filterCount >= 100, 422, self::FILTER_LIMIT); + } $type = $request->input('type'); $item = $request->input('item'); $action = $type . '.mute'; @@ -237,6 +247,12 @@ class AccountController extends Controller ]); $user = Auth::user()->profile; + $count = UserFilterService::blockCount($user->id); + abort_if($count >= 100, 422, self::FILTER_LIMIT); + if($count == 0) { + $filterCount = UserFilter::whereUserId($user->id)->count(); + abort_if($filterCount >= 100, 422, self::FILTER_LIMIT); + } $type = $request->input('type'); $item = $request->input('item'); $action = $type.'.block'; @@ -552,5 +568,21 @@ class AccountController extends Controller $prev = $page > 1 ? $page - 1 : 1; $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"'; return response()->json($res, 200, ['Link' => $links]); + + } + + public function accountBlocksV2(Request $request) + { + return response()->json(UserFilterService::blocks($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES); + } + + public function accountMutesV2(Request $request) + { + return response()->json(UserFilterService::mutes($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES); + } + + public function accountFiltersV2(Request $request) + { + return response()->json(UserFilterService::filters($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES); } } diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 5e737e9cd..e8a27fc67 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -563,9 +563,12 @@ class ApiV1Controller extends Controller 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX ]); $pid = $request->user()->profile_id ?? $request->user()->profile->id; - $ids = collect($request->input('id')); - $res = $ids->map(function($id) use($pid) { - return RelationshipService::get($pid, $id); + $res = collect($request->input('id')) + ->filter(function($id) use($pid) { + return $id != $pid; + }) + ->map(function($id) use($pid) { + return RelationshipService::get($pid, $id); }); return response()->json($res); } @@ -1485,13 +1488,13 @@ class ApiV1Controller extends Controller $limit = $request->input('limit') ?? 3; $user = $request->user(); - Cache::remember('api:v1:timelines:public:cache_check', 3600, function() { + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { if(PublicTimelineService::count() == 0) { - PublicTimelineService::warmCache(true, 400); - } + PublicTimelineService::warmCache(true, 400); + } }); - if ($max) { + if ($max) { $feed = PublicTimelineService::getRankedMaxId($max, $limit); } else if ($min) { $feed = PublicTimelineService::getRankedMinId($min, $limit); @@ -1500,14 +1503,15 @@ class ApiV1Controller extends Controller } $res = collect($feed) - ->map(function($k) use($user) { - $status = StatusService::get($k); - if($user) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); - } - return $status; - }) - ->toArray(); + ->map(function($k) use($user) { + $status = StatusService::get($k); + if($user) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); + $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']); + } + return $status; + }) + ->toArray(); return response()->json($res); } diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index 6a814388a..7a6ea3990 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -30,6 +30,7 @@ use App\Services\{ LikeService, PublicTimelineService, ProfileService, + RelationshipService, StatusService, SnowflakeService, UserFilterService @@ -288,69 +289,30 @@ class PublicApiController extends Controller $limit = $request->input('limit') ?? 3; $user = $request->user(); - $filtered = $user ? UserFilterService::filters($user->profile_id) : []; + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { + if(PublicTimelineService::count() == 0) { + PublicTimelineService::warmCache(true, 400); + } + }); - if($min || $max) { - $dir = $min ? '>' : '<'; - $id = $min ?? $max; - $timeline = Status::select( - 'id', - 'profile_id', - 'type', - 'scope', - 'local' - ) - ->where('id', $dir, $id) - ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) - ->whereNotIn('profile_id', $filtered) - ->whereLocal(true) - ->whereScope('public') - ->orderBy('id', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - $status = StatusService::getFull($s->id, $user->profile_id); - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - return $status; - }); - $res = $timeline->toArray(); - } else { - $timeline = Status::select( - 'id', - 'uri', - 'caption', - 'rendered', - 'profile_id', - 'type', - 'in_reply_to_id', - 'reblog_of_id', - 'is_nsfw', - 'scope', - 'local', - 'reply_count', - 'comments_disabled', - 'created_at', - 'place_id', - 'likes_count', - 'reblogs_count', - 'updated_at' - ) - ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) - ->whereNotIn('profile_id', $filtered) - ->with('profile', 'hashtags', 'mentions') - ->whereLocal(true) - ->whereScope('public') - ->orderBy('id', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - $status = StatusService::getFull($s->id, $user->profile_id); - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - return $status; - }); + if ($max) { + $feed = PublicTimelineService::getRankedMaxId($max, $limit); + } else if ($min) { + $feed = PublicTimelineService::getRankedMinId($min, $limit); + } else { + $feed = PublicTimelineService::get(0, $limit); + } - $res = $timeline->toArray(); - } + $res = collect($feed) + ->map(function($k) use($user) { + $status = StatusService::get($k); + if($user) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); + $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']); + } + return $status; + }) + ->toArray(); return response()->json($res); } @@ -580,17 +542,20 @@ class PublicApiController extends Controller return response()->json([]); } + $pid = $request->user()->profile_id; + $this->validate($request, [ 'id' => 'required|array|min:1|max:20', 'id.*' => 'required|integer' ]); $ids = collect($request->input('id')); - $filtered = $ids->filter(function($v) { - return $v != Auth::user()->profile->id; + $res = $ids->filter(function($v) use($pid) { + return $v != $pid; + }) + ->map(function($id) use($pid) { + return RelationshipService::get($pid, $id); }); - $relations = Profile::whereNull('status')->findOrFail($filtered->all()); - $fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer()); - $res = $this->fractal->createData($fractal)->toArray(); + return response()->json($res); } @@ -741,5 +706,4 @@ class PublicApiController extends Controller return response()->json($res); } - } diff --git a/app/Services/AccountService.php b/app/Services/AccountService.php index d65f95943..d046b8c85 100644 --- a/app/Services/AccountService.php +++ b/app/Services/AccountService.php @@ -8,6 +8,8 @@ use App\Status; use App\Transformer\Api\AccountTransformer; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; class AccountService { @@ -62,4 +64,22 @@ class AccountService Cache::put($key, 1, 900); return true; } + + public static function usernameToId($username) + { + $key = self::CACHE_KEY . 'u2id:' . hash('sha256', $username); + return Cache::remember($key, 900, function() use($username) { + $s = Str::of($username); + if($s->contains('@') && !$s->startsWith('@')) { + $username = "@{$username}"; + } + $profile = DB::table('profiles') + ->whereUsername($username) + ->first(); + if(!$profile) { + return null; + } + return (string) $profile->id; + }); + } } diff --git a/app/Services/UserFilterService.php b/app/Services/UserFilterService.php index 4d155fa69..49118b579 100644 --- a/app/Services/UserFilterService.php +++ b/app/Services/UserFilterService.php @@ -98,4 +98,14 @@ class UserFilterService { } return $exists; } + + public static function blockCount(int $profile_id) + { + return Redis::zcard(self::USER_BLOCKS_KEY . $profile_id); + } + + public static function muteCount(int $profile_id) + { + return Redis::zcard(self::USER_MUTES_KEY . $profile_id); + } } diff --git a/routes/web.php b/routes/web.php index e4afd5961..0a1b50850 100644 --- a/routes/web.php +++ b/routes/web.php @@ -202,6 +202,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::post('status/{id}/archive', 'ApiController@archive'); Route::post('status/{id}/unarchive', 'ApiController@unarchive'); Route::get('statuses/archives', 'ApiController@archivedPosts'); + Route::get('mutes', 'AccountController@accountMutesV2'); + Route::get('blocks', 'AccountController@accountBlocksV2'); + Route::get('filters', 'AccountController@accountFiltersV2'); }); });