diff --git a/CHANGELOG.md b/CHANGELOG.md index 377781f68..2f4302b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,10 @@ - Update ApiV1Controller, fix typo in statavouriteById method ([c91a6a75](https://github.com/pixelfed/pixelfed/commit/c91a6a75)) - Update InboxPipeline, fix peertube attributedTo parsing ([99fb80bf](https://github.com/pixelfed/pixelfed/commit/99fb80bf)) - Update Collection components, fix addId bug #3230 ([62c05665](https://github.com/pixelfed/pixelfed/commit/62c05665)) +- Update DirectMessageController, include account entity in lookup endpoint ([9e223a6b](https://github.com/pixelfed/pixelfed/commit/9e223a6b)) +- Update ApiV1Controller update_credentials endpoint to support app response ([61d26e85](https://github.com/pixelfed/pixelfed/commit/61d26e85)) +- Update PronounService, fix json_decode null parameter ([d72cd819](https://github.com/pixelfed/pixelfed/commit/d72cd819)) +- Update ApiV1Controller, normalize profile id comparison ([374bfdae](https://github.com/pixelfed/pixelfed/commit/374bfdae)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.3 (2022-05-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.2...v0.11.3) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 54d31044c..e4da6a0ed 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -65,6 +65,7 @@ use App\Services\{ NetworkTimelineService, NotificationService, MediaPathService, + ProfileStatusService, PublicTimelineService, ReblogService, RelationshipService, @@ -432,9 +433,13 @@ class ApiV1Controller extends Controller MediaSyncLicensePipeline::dispatch($user->id, $request->input('license')); } - $res = AccountService::getMastodon($user->profile_id); - $res['bio'] = strip_tags($res['note']); - $res = array_merge($res, $other); + if($request->has(self::PF_API_ENTITY_KEY)) { + $res = AccountService::get($user->profile_id, true); + } else { + $res = AccountService::getMastodon($user->profile_id, true); + $res['bio'] = strip_tags($res['note']); + $res = array_merge($res, $other); + } return $this->json($res); } @@ -453,7 +458,7 @@ class ApiV1Controller extends Controller abort_if(!$account, 404); $pid = $request->user()->profile_id; - if($pid != $account['id']) { + if(intval($pid) !== intval($account['id'])) { if($account['locked']) { if(!FollowerService::follows($pid, $account['id'])) { return []; @@ -500,7 +505,7 @@ class ApiV1Controller extends Controller abort_if(!$account, 404); $pid = $request->user()->profile_id; - if($pid != $account['id']) { + if(intval($pid) !== intval($account['id'])) { if($account['locked']) { if(!FollowerService::follows($pid, $account['id'])) { return []; @@ -559,7 +564,7 @@ class ApiV1Controller extends Controller $profile = $napi ? AccountService::get($id, true) : AccountService::getMastodon($id, true); if(!$profile || !isset($profile['id']) || !$user) { - return response('', 404); + return $this->json(['error' => 'Account not found'], 404); } $limit = $request->limit ?? 20; @@ -582,7 +587,7 @@ class ApiV1Controller extends Controller } } - if($pid == $profile['id']) { + if(intval($pid) === intval($profile['id'])) { $visibility = ['public', 'unlisted', 'private']; } else if($profile['locked']) { $following = FollowerService::follows($pid, $profile['id']); @@ -812,7 +817,7 @@ class ApiV1Controller extends Controller $pid = $request->user()->profile_id ?? $request->user()->profile->id; $res = collect($request->input('id')) ->filter(function($id) use($pid) { - return $id != $pid; + return intval($id) !== intval($pid); }) ->map(function($id) use($pid) { return RelationshipService::get($pid, $id); @@ -843,15 +848,21 @@ class ApiV1Controller extends Controller $resolve = (bool) $request->input('resolve', false); $q = '%' . $query . '%'; - $profiles = Profile::whereNull('status') - ->where('username', 'like', $q) - ->orWhere('name', 'like', $q) - ->limit($limit) - ->get(); + $profiles = Cache::remember('api:v1:accounts:search:' . sha1($query) . ':limit:' . $limit, 86400, function() use($q, $limit) { + return Profile::whereNull('status') + ->where('username', 'like', $q) + ->orWhere('name', 'like', $q) + ->limit($limit) + ->pluck('id') + ->map(function($id) { + return AccountService::getMastodon($id); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }); + }); - $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - return $this->json($res); + return $this->json($profiles); } /** @@ -903,7 +914,7 @@ class ApiV1Controller extends Controller $user = $request->user(); $pid = $user->profile_id ?? $user->profile->id; - if($id == $pid) { + if(intval($id) === intval($pid)) { abort(400, 'You cannot block yourself'); } @@ -948,7 +959,7 @@ class ApiV1Controller extends Controller $user = $request->user(); $pid = $user->profile_id ?? $user->profile->id; - if($id == $pid) { + if(intval($id) === intval($pid)) { abort(400, 'You cannot unblock yourself'); } @@ -1083,7 +1094,7 @@ class ApiV1Controller extends Controller $spid = $status['account']['id']; - if($spid !== $user->profile_id) { + if(intval($spid) !== intval($user->profile_id)) { if($status['visibility'] == 'private') { abort_if(!FollowerService::follows($user->profile_id, $spid), 403); } else { @@ -1138,7 +1149,7 @@ class ApiV1Controller extends Controller $status = Status::findOrFail($id); - if($status->profile_id !== $user->profile_id) { + if(intval($status->profile_id) !== intval($user->profile_id)) { if($status->scope == 'private') { abort_if(!$status->profile->followedBy($user->profile), 403); } else { @@ -1765,6 +1776,10 @@ class ApiV1Controller extends Controller $user = $request->user(); $pid = $user->profile_id; + if(intval($pid) === intval($id)) { + return $this->json(['error' => 'You cannot mute yourself'], 500); + } + $account = Profile::findOrFail($id); $filter = UserFilter::firstOrCreate([ @@ -1798,6 +1813,10 @@ class ApiV1Controller extends Controller $user = $request->user(); $pid = $user->profile_id; + if(intval($pid) === intval($id)) { + return $this->json(['error' => 'You cannot unmute yourself'], 500); + } + $account = Profile::findOrFail($id); $filter = UserFilter::whereUserId($pid) @@ -2223,7 +2242,7 @@ class ApiV1Controller extends Controller $scope = $res['visibility']; if(!in_array($scope, ['public', 'unlisted'])) { if($scope === 'private') { - if($res['account']['id'] != $user->profile_id) { + if(intval($res['account']['id']) !== intval($user->profile_id)) { abort_unless(FollowerService::follows($user->profile_id, $res['account']['id']), 403); } } else { @@ -2256,7 +2275,7 @@ class ApiV1Controller extends Controller return response('', 404); } - if($status['account']['id'] != $user->profile_id) { + if(intval($status['account']['id']) !== intval($user->profile_id)) { if($status['visibility'] == 'private') { if(!FollowerService::follows($user->profile_id, $status['account']['id'])) { return response('', 404); @@ -2336,7 +2355,7 @@ class ApiV1Controller extends Controller $user = $request->user(); $status = Status::findOrFail($id); - if($status->profile_id !== $user->profile_id) { + if(intval($status->profile_id) !== intval($user->profile_id)) { if($status->scope == 'private') { abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); } else { @@ -2402,7 +2421,7 @@ class ApiV1Controller extends Controller } } - if($status->profile_id !== $user->profile_id) { + if(intval($status->profile_id) !== intval($user->profile_id)) { if($status->scope == 'private') { abort_if(!$status->profile->followedBy($user->profile), 403); } else { @@ -2639,7 +2658,7 @@ class ApiV1Controller extends Controller $user = $request->user(); $status = Status::whereScope('public')->findOrFail($id); - if($status->profile_id !== $user->profile_id) { + if(intval($status->profile_id) !== intval($user->profile_id)) { if($status->scope == 'private') { abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); } else { @@ -2687,7 +2706,7 @@ class ApiV1Controller extends Controller $user = $request->user(); $status = Status::whereScope('public')->findOrFail($id); - if($status->profile_id !== $user->profile_id) { + if(intval($status->profile_id) !== intval($user->profile_id)) { if($status->scope == 'private') { abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); } else { @@ -3026,7 +3045,7 @@ class ApiV1Controller extends Controller } /** - * GET /api/v1/discover/accounts/popular + * GET /api/v1.1/discover/accounts/popular * * * @return array @@ -3054,6 +3073,18 @@ class ApiV1Controller extends Controller ->filter(function($profile) use($pid) { return $profile['id'] != $pid; }) + ->map(function($profile) { + $ids = collect(ProfileStatusService::get($profile['id'], 0, 9)) + ->map(function($id) { + return StatusService::get($id, true); + }) + ->filter(function($post) { + return $post && isset($post['id']); + }) + ->take(3); + $profile['recent_posts'] = $ids; + return $profile; + }) ->take(6) ->values(); diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php index 5a1afbc8c..8c367e726 100644 --- a/app/Http/Controllers/DirectMessageController.php +++ b/app/Http/Controllers/DirectMessageController.php @@ -704,12 +704,14 @@ class DirectMessageController extends Controller ->limit(8) ->get() ->map(function($r) { + $acct = AccountService::get($r->id); return [ 'local' => (bool) !$r->domain, 'id' => (string) $r->id, 'name' => $r->username, 'privacy' => true, - 'avatar' => $r->avatarUrl() + 'avatar' => $r->avatarUrl(), + 'account' => $acct ]; }); diff --git a/app/Observers/StatusObserver.php b/app/Observers/StatusObserver.php new file mode 100644 index 000000000..a5bd84aee --- /dev/null +++ b/app/Observers/StatusObserver.php @@ -0,0 +1,66 @@ +scope, ['public', 'unlisted']) && in_array($status->type, ['photo', 'photo:album', 'video'])) { + ProfileStatusService::add($status->profile_id, $status->id); + } + } + + /** + * Handle the Status "deleted" event. + * + * @param \App\Status $status + * @return void + */ + public function deleted(Status $status) + { + ProfileStatusService::delete($status->profile_id, $status->id); + } + + /** + * Handle the Status "restored" event. + * + * @param \App\Status $status + * @return void + */ + public function restored(Status $status) + { + // + } + + /** + * Handle the Status "force deleted" event. + * + * @param \App\Status $status + * @return void + */ + public function forceDeleted(Status $status) + { + // + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index a4dfbe27b..2272efa5e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -8,7 +8,8 @@ use App\Observers\{ NotificationObserver, ModLogObserver, ProfileObserver, - StatusHashtagObserver, + StatusHashtagObserver, + StatusObserver, UserObserver, UserFilterObserver, }; @@ -19,6 +20,7 @@ use App\{ ModLog, Profile, StatusHashtag, + Status, User, UserFilter }; @@ -47,6 +49,7 @@ class AppServiceProvider extends ServiceProvider Profile::observe(ProfileObserver::class); StatusHashtag::observe(StatusHashtagObserver::class); User::observe(UserObserver::class); + Status::observe(StatusObserver::class); UserFilter::observe(UserFilterObserver::class); Horizon::auth(function ($request) { return Auth::check() && $request->user()->is_admin; diff --git a/app/Services/ProfileStatusService.php b/app/Services/ProfileStatusService.php new file mode 100644 index 000000000..907530dbc --- /dev/null +++ b/app/Services/ProfileStatusService.php @@ -0,0 +1,66 @@ + self::FALLOFF_LIMIT) { + Redis::zpopmin(self::CACHE_KEY . $pid); + } + return Redis::zadd(self::CACHE_KEY . $pid, $sid, $sid); + } + + public static function delete($pid, $sid) + { + return Redis::zrem(self::CACHE_KEY . $pid, $sid); + } + + public static function coldFetch($pid) + { + Redis::del(self::CACHE_KEY . $pid); + $ids = DB::table('statuses') + ->select('id', 'profile_id', 'type', 'scope') + ->whereIn('type', ['photo', 'photo:album', 'video']) + ->whereIn('scope', ['public', 'unlisted']) + ->whereProfileId($pid) + ->orderByDesc('id') + ->limit(self::FALLOFF_LIMIT) + ->pluck('id') + ->toArray(); + + if($ids && count($ids)) { + foreach($ids as $id) { + self::add($pid, $id); + } + } + Redis::zadd(self::COLD_CHECK_KEY, $pid, $pid); + return $ids; + } +} diff --git a/app/Services/PronounService.php b/app/Services/PronounService.php index f8185b97b..dce1d591a 100644 --- a/app/Services/PronounService.php +++ b/app/Services/PronounService.php @@ -15,7 +15,7 @@ class PronounService { return Cache::remember($key, $ttl, function() use($id) { $res = UserPronoun::whereProfileId($id)->first(); - return $res ? json_decode($res->pronouns, true) : []; + return $res && $res->pronouns ? json_decode($res->pronouns, true) : []; }); } diff --git a/routes/api.php b/routes/api.php index 805fab0f0..95c822562 100644 --- a/routes/api.php +++ b/routes/api.php @@ -100,8 +100,17 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::group(['prefix' => 'v1.1'], function() use($middleware) { Route::post('report', 'Api\ApiV1Dot1Controller@report')->middleware($middleware); Route::delete('accounts/avatar', 'Api\ApiV1Dot1Controller@deleteAvatar')->middleware($middleware); - Route::get('direct/thread', 'DirectMessageController@thread')->middleware($middleware); - Route::post('direct/thread/send', 'DirectMessageController@create')->middleware($middleware); + + Route::group(['prefix' => 'direct'], function () use($middleware) { + Route::get('thread', 'DirectMessageController@thread')->middleware($middleware); + Route::post('thread/send', 'DirectMessageController@create')->middleware($middleware); + Route::delete('thread/message', 'DirectMessageController@delete')->middleware($middleware); + Route::post('thread/mute', 'DirectMessageController@mute')->middleware($middleware); + Route::post('thread/unmute', 'DirectMessageController@unmute')->middleware($middleware); + Route::post('thread/media', 'DirectMessageController@mediaUpload')->middleware($middleware); + Route::post('thread/read', 'DirectMessageController@read')->middleware($middleware); + Route::post('lookup', 'DirectMessageController@composeLookup')->middleware($middleware); + }); Route::group(['prefix' => 'stories'], function () use($middleware) { Route::get('recent', 'StoryController@recent')->middleware($middleware);