diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b96459d..b7e29ac07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,18 @@ - Updated Status model, use AccountService to generate urls instead of loading profile relation. ([2ae527c0](https://github.com/pixelfed/pixelfed/commit/2ae527c0)) - Updated Autospam service, add mark all as read and mark all as not spam options and filter active, spam and not spam reports. ([ae8c7517](https://github.com/pixelfed/pixelfed/commit/ae8c7517)) - Updated UserInviteController, fixes #3017. ([b8e9056e](https://github.com/pixelfed/pixelfed/commit/b8e9056e)) +- Updated AccountService, add dynamic user settings methods. ([2aa73c1f](https://github.com/pixelfed/pixelfed/commit/2aa73c1f)) +- Updated MediaStorageService, improve header parsing. ([9d9e9ce7](https://github.com/pixelfed/pixelfed/commit/9d9e9ce7)) +- Updated SearchApiV2Service, improve performance and include hashtag post counts when applicable ([fbaed93e](https://github.com/pixelfed/pixelfed/commit/fbaed93e)) +- Updated AccountTransformer, add note_text and location fields. ([98f76abb](https://github.com/pixelfed/pixelfed/commit/98f76abb)) +- Updated UserSetting model, cast compose_settings and other as json. ([03420278](https://github.com/pixelfed/pixelfed/commit/03420278)) +- Updated ApiV1Controller, improve settings and add discoverPosts endpoint. ([079804e6](https://github.com/pixelfed/pixelfed/commit/079804e6)) +- Updated LikePipeline jobs, fix likes_count calculation. ([fe64e187](https://github.com/pixelfed/pixelfed/commit/fe64e187)) +- Updated InternalApiController, prevent moderation actions against admin accounts. ([945a7e49](https://github.com/pixelfed/pixelfed/commit/945a7e49)) +- Updated CommentPipeline, move reply_count calculation to comment pipeline job and improve count calculation. ([b6b0837f](https://github.com/pixelfed/pixelfed/commit/b6b0837f)) +- Updated ApiV1Controller, improve statusesById perf and dispatch CommentPipeline job when applicable. ([466286af](https://github.com/pixelfed/pixelfed/commit/466286af)) +- Updated MediaService, return empty array if cant find status. ([c2910e5d](https://github.com/pixelfed/pixelfed/commit/c2910e5d)) +- Updated StatusService, improve cache invalidation. ([83b48b56](https://github.com/pixelfed/pixelfed/commit/83b48b56)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1) diff --git a/app/Http/Controllers/Admin/AdminReportController.php b/app/Http/Controllers/Admin/AdminReportController.php index 484d80b46..93e759423 100644 --- a/app/Http/Controllers/Admin/AdminReportController.php +++ b/app/Http/Controllers/Admin/AdminReportController.php @@ -296,7 +296,7 @@ trait AdminReportController $status->scope = 'public'; $status->visibility = 'public'; $status->save(); - StatusService::del($status->id); + StatusService::del($status->id, true); } }); Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); @@ -363,7 +363,7 @@ trait AdminReportController $appeal->appeal_handled_at = now(); $appeal->save(); - StatusService::del($status->id); + StatusService::del($status->id, true); Cache::forget('admin-dash:reports:ai-count'); return redirect('/i/admin/reports/appeals'); @@ -413,20 +413,20 @@ trait AdminReportController $item->is_nsfw = true; $item->save(); $report->nsfw = true; - StatusService::del($item->id); + StatusService::del($item->id, true); break; case 'unlist': $item->visibility = 'unlisted'; $item->save(); Cache::forget('profiles:private'); - StatusService::del($item->id); + StatusService::del($item->id, true); break; case 'delete': // Todo: fire delete job $report->admin_seen = null; - StatusService::del($item->id); + StatusService::del($item->id, true); break; case 'shadowban': diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index b4a7a0781..1600fa20f 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -10,7 +10,9 @@ use App\Util\Media\Filter; use Laravel\Passport\Passport; use Auth, Cache, DB, URL; use App\{ + Avatar, Bookmark, + DirectMessage, Follower, FollowRequest, Hashtag, @@ -38,6 +40,9 @@ use App\Http\Controllers\FollowerController; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Http\Controllers\StatusController; + +use App\Jobs\AvatarPipeline\AvatarOptimize; +use App\Jobs\CommentPipeline\CommentPipeline; use App\Jobs\LikePipeline\LikePipeline; use App\Jobs\SharePipeline\SharePipeline; use App\Jobs\StatusPipeline\NewStatusPipeline; @@ -49,8 +54,11 @@ use App\Jobs\VideoPipeline\{ VideoPostProcess, VideoThumbnail }; + use App\Services\{ + AccountService, LikeService, + InstanceService, NotificationService, MediaPathService, PublicTimelineService, @@ -59,9 +67,13 @@ use App\Services\{ SearchApiV2Service, StatusService, MediaBlocklistService, + SnowflakeService, UserFilterService }; use App\Util\Lexer\Autolink; +use App\Util\Localization\Localization; +use App\Util\Media\License; +use App\Jobs\MediaPipeline\MediaSyncLicensePipeline; class ApiV1Controller extends Controller { @@ -166,47 +178,222 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $this->validate($request, [ + 'avatar' => 'sometimes|mimetypes:image/jpeg,image/png', 'display_name' => 'nullable|string', 'note' => 'nullable|string', 'locked' => 'nullable', + 'website' => 'nullable', // 'source.privacy' => 'nullable|in:unlisted,public,private', // 'source.sensitive' => 'nullable|boolean' ]); $user = $request->user(); $profile = $user->profile; - - $displayName = $request->input('display_name'); - $note = $request->input('note'); - $locked = $request->input('locked'); - // $privacy = $request->input('source.privacy'); - // $sensitive = $request->input('source.sensitive'); + $settings = $user->settings; $changes = false; + $other = array_merge(AccountService::defaultSettings()['other'], $settings->other ?? []); + $syncLicenses = false; + $licenseChanged = false; + $composeSettings = array_merge(AccountService::defaultSettings()['compose_settings'], $settings->compose_settings ?? []); - if($displayName !== $user->name) { - $user->name = $displayName; - $profile->name = $displayName; + // return $request->input('locked'); + + if($request->has('avatar')) { + $av = Avatar::whereProfileId($profile->id)->first(); + if($av) { + $currentAvatar = storage_path('app/'.$av->media_path); + $file = $request->file('avatar'); + $path = "public/avatars/{$profile->id}"; + $name = strtolower(str_random(6)). '.' . $file->guessExtension(); + $request->file('avatar')->storeAs($path, $name); + $av->media_path = "{$path}/{$name}"; + $av->save(); + Cache::forget("avatar:{$profile->id}"); + Cache::forget('user:account:id:'.$user->id); + AvatarOptimize::dispatch($user->profile, $currentAvatar); + } $changes = true; } - if($note !== strip_tags($profile->bio)) { - $profile->bio = Autolink::create()->autolink(strip_tags($note)); - $changes = true; + if($request->has('source[language]')) { + $lang = $request->input('source[language]'); + if(in_array($lang, Localization::languages())) { + $user->language = $lang; + $changes = true; + $other['language'] = $lang; + } } - if(!is_null($locked)) { - $profile->is_private = $locked; - $changes = true; + if($request->has('website')) { + $website = $request->input('website'); + if($website != $profile->website) { + if($website) { + if(!strpos($website, '.')) { + $website = null; + } + + if($website && !strpos($website, '://')) { + $website = 'https://' . $website; + } + + $host = parse_url($website, PHP_URL_HOST); + + $bannedInstances = InstanceService::getBannedDomains(); + if(in_array($host, $bannedInstances)) { + $website = null; + } + } + $profile->website = $website ? $website : null; + $changes = true; + } + } + + if($request->has('display_name')) { + $displayName = $request->input('display_name'); + if($displayName !== $user->name) { + $user->name = $displayName; + $profile->name = $displayName; + $changes = true; + } + } + + if($request->has('note')) { + $note = $request->input('note'); + if($note !== strip_tags($profile->bio)) { + $profile->bio = Autolink::create()->autolink(strip_tags($note)); + $changes = true; + } + } + + if($request->has('locked')) { + $locked = $request->input('locked') == 'true'; + if($profile->is_private != $locked) { + $profile->is_private = $locked; + $changes = true; + } + } + + if($request->has('reduce_motion')) { + $reduced = $request->input('reduce_motion'); + if($settings->reduce_motion != $reduced) { + $settings->reduce_motion = $reduced; + $changes = true; + } + } + + if($request->has('high_contrast_mode')) { + $contrast = $request->input('high_contrast_mode'); + if($settings->high_contrast_mode != $contrast) { + $settings->high_contrast_mode = $contrast; + $changes = true; + } + } + + if($request->has('video_autoplay')) { + $autoplay = $request->input('video_autoplay'); + if($settings->video_autoplay != $autoplay) { + $settings->video_autoplay = $autoplay; + $changes = true; + } + } + + if($request->has('license')) { + $license = $request->input('license'); + abort_if(!in_array($license, License::keys()), 422, 'Invalid media license id'); + $syncLicenses = $request->input('sync_licenses') == true; + abort_if($syncLicenses && Cache::get('pf:settings:mls_recently:'.$user->id) == 2, 422, 'You can only sync licenses twice per 24 hours'); + if($composeSettings['default_license'] != $license) { + $composeSettings['default_license'] = $license; + $licenseChanged = true; + $changes = true; + } + } + + if($request->has('media_descriptions')) { + $md = $request->input('media_descriptions') == true; + if($composeSettings['media_descriptions'] != $md) { + $composeSettings['media_descriptions'] = $md; + $changes = true; + } + } + + if($request->has('crawlable')) { + $crawlable = $request->input('crawlable'); + if($settings->crawlable != $crawlable) { + $settings->crawlable = $crawlable; + $changes = true; + } + } + + if($request->has('show_profile_follower_count')) { + $show_profile_follower_count = $request->input('show_profile_follower_count'); + if($settings->show_profile_follower_count != $show_profile_follower_count) { + $settings->show_profile_follower_count = $show_profile_follower_count; + $changes = true; + } + } + + if($request->has('show_profile_following_count')) { + $show_profile_following_count = $request->input('show_profile_following_count'); + if($settings->show_profile_following_count != $show_profile_following_count) { + $settings->show_profile_following_count = $show_profile_following_count; + $changes = true; + } + } + + if($request->has('public_dm')) { + $public_dm = $request->input('public_dm'); + if($settings->public_dm != $public_dm) { + $settings->public_dm = $public_dm; + $changes = true; + } + } + + if($request->has('source[privacy]')) { + $scope = $request->input('source[privacy]'); + if(in_array($scope, ['public', 'private', 'unlisted'])) { + if($composeSettings['default_scope'] != $scope) { + $composeSettings['default_scope'] = $profile->is_private ? 'private' : $scope; + $changes = true; + } + } + } + + if($request->has('disable_embeds')) { + $disabledEmbeds = $request->input('disable_embeds'); + if($other['disable_embeds'] != $disabledEmbeds) { + $other['disable_embeds'] = $disabledEmbeds; + $changes = true; + } } if($changes) { + $settings->other = $other; + $settings->compose_settings = $composeSettings; + $settings->save(); $user->save(); $profile->save(); + Cache::forget('profile:settings:' . $profile->id); + Cache::forget('user:account:id:' . $profile->user_id); + Cache::forget('profile:follower_count:' . $profile->id); + Cache::forget('profile:following_count:' . $profile->id); + Cache::forget('profile:embed:' . $profile->id); + Cache::forget('profile:compose:settings:' . $user->id); + Cache::forget('profile:view:'.$user->username); + AccountService::del($user->profile_id); } - $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); - $res = $this->fractal->createData($resource)->toArray(); + if($syncLicenses && $licenseChanged) { + $key = 'pf:settings:mls_recently:'.$user->id; + $val = Cache::has($key) ? 2 : 1; + Cache::put($key, $val, 86400); + MediaSyncLicensePipeline::dispatch($user->id, $request->input('license')); + } + + $res = AccountService::get($user->profile_id); + $res['bio'] = strip_tags($res['note']); + $res = array_merge($res, $other); return response()->json($res); } @@ -305,9 +492,11 @@ class ApiV1Controller extends Controller public function accountStatusesById(Request $request, $id) { abort_if(!$request->user(), 403); + $user = $request->user(); $this->validate($request, [ 'only_media' => 'nullable', + 'media_type' => 'sometimes|string|in:photo,video', 'pinned' => 'nullable', 'exclude_replies' => 'nullable', 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, @@ -316,7 +505,8 @@ class ApiV1Controller extends Controller 'limit' => 'nullable|integer|min:1|max:80' ]); - $profile = Profile::whereNull('status')->findOrFail($id); + $profile = AccountService::get($id); + abort_if(!$profile, 404); $limit = $request->limit ?? 20; $max_id = $request->max_id; @@ -326,77 +516,56 @@ class ApiV1Controller extends Controller ['photo', 'photo:album', 'video', 'video:album'] : ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply']; - if($pid == $profile->id) { + if($request->only_media && $request->has('media_type')) { + $mt = $request->input('media_type'); + if($mt == 'video') { + $scope = ['video', 'video:album']; + } + } + + if($pid == $profile['id']) { $visibility = ['public', 'unlisted', 'private']; - } else if($profile->is_private) { + } else if($profile['locked']) { $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) { $following = Follower::whereProfileId($pid)->pluck('following_id'); return $following->push($pid)->toArray(); }); - $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : []; + $visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : []; } else { $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) { $following = Follower::whereProfileId($pid)->pluck('following_id'); return $following->push($pid)->toArray(); }); - $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; + $visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; } - if($min_id || $max_id) { - $dir = $min_id ? '>' : '<'; - $id = $min_id ?? $max_id; - $timeline = Status::select( - 'id', - 'uri', - 'caption', - 'rendered', - 'profile_id', - 'type', - 'in_reply_to_id', - 'reblog_of_id', - 'is_nsfw', - 'scope', - 'local', - 'place_id', - 'likes_count', - 'reblogs_count', - 'created_at', - 'updated_at' - )->whereProfileId($profile->id) - ->whereIn('type', $scope) - ->where('id', $dir, $id) - ->whereIn('visibility', $visibility) - ->latest() - ->limit($limit) - ->get(); - } else { - $timeline = Status::select( - 'id', - 'uri', - 'caption', - 'rendered', - 'profile_id', - 'type', - 'in_reply_to_id', - 'reblog_of_id', - 'is_nsfw', - 'scope', - 'local', - 'place_id', - 'likes_count', - 'reblogs_count', - 'created_at', - 'updated_at' - )->whereProfileId($profile->id) - ->whereIn('type', $scope) - ->whereIn('visibility', $visibility) - ->latest() - ->limit($limit) - ->get(); - } + $dir = $min_id ? '>' : '<'; + $id = $min_id ?? $max_id; + $res = Status::whereProfileId($profile['id']) + ->whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->whereIn('type', $scope) + ->where('id', $dir, $id) + ->whereIn('scope', $visibility) + ->limit($limit) + ->orderByDesc('id') + ->get() + ->map(function($s) use($user) { + try { + $status = StatusService::get($s->id, false); + } catch (\Exception $e) { + $status = false; + } + if($user && $status) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + } + return $status; + }) + ->filter(function($s) { + return $s; + }) + ->values(); - $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer()); - $res = $this->fractal->createData($resource)->toArray(); return response()->json($res); } @@ -417,6 +586,7 @@ class ApiV1Controller extends Controller ->whereNull('status') ->findOrFail($id); + $private = (bool) $target->is_private; $remote = (bool) $target->domain; $blocked = UserFilter::whereUserId($target->id) @@ -435,9 +605,7 @@ class ApiV1Controller extends Controller // Following already, return empty relationship if($isFollowing == true) { - $resource = new Fractal\Resource\Item($target, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - + $res = RelationshipService::get($user->profile_id, $target->id) ?? []; return response()->json($res); } @@ -471,6 +639,7 @@ class ApiV1Controller extends Controller FollowPipeline::dispatch($follower); } + RelationshipService::refresh($user->profile_id, $target->id); Cache::forget('profile:following:'.$target->id); Cache::forget('profile:followers:'.$target->id); Cache::forget('profile:following:'.$user->profile_id); @@ -483,8 +652,7 @@ class ApiV1Controller extends Controller Cache::forget('profile:following_count:'.$target->id); Cache::forget('profile:following_count:'.$user->profile_id); - $resource = new Fractal\Resource\Item($target, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); + $res = RelationshipService::get($user->profile_id, $target->id); return response()->json($res); } @@ -506,6 +674,8 @@ class ApiV1Controller extends Controller ->whereNull('status') ->findOrFail($id); + RelationshipService::refresh($user->profile_id, $target->id); + $private = (bool) $target->is_private; $remote = (bool) $target->domain; @@ -770,19 +940,53 @@ class ApiV1Controller extends Controller public function accountFavourites(Request $request) { abort_if(!$request->user(), 403); + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:20' + ]); $user = $request->user(); + $maxId = $request->input('max_id'); + $minId = $request->input('min_id'); + $limit = $request->input('limit') ?? 10; - $limit = $request->input('limit') ?? 20; - $favourites = Like::whereProfileId($user->profile_id) - ->latest() - ->simplePaginate($limit) - ->pluck('status_id'); + $res = Like::whereProfileId($user->profile_id) + ->when($maxId, function($q, $maxId) { + return $q->where('id', '<', $maxId); + }) + ->when($minId, function($q, $minId) { + return $q->where('id', '>', $minId); + }) + ->orderByDesc('id') + ->limit($limit) + ->get() + ->map(function($like) { + $status = StatusService::get($like['status_id'], false); + $status['like_id'] = $like->id; + $status['liked_at'] = $like->created_at->format('c'); + return $status; + }) + ->filter(function($status) { + return $status && isset($status['id'], $status['like_id']); + }) + ->values(); - $statuses = Status::findOrFail($favourites); - $resource = new Fractal\Resource\Collection($statuses, new StatusTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - return response()->json($res); + if($res->count()) { + $ids = $res->map(function($status) { + return $status['like_id']; + }); + $max = $ids->max(); + $min = $ids->min(); + + $baseUrl = config('app.url') . '/api/v1/favourites?limit=' . $limit . '&'; + $link = '<'.$baseUrl.'max_id='.$max.'>; rel="next",<'.$baseUrl.'min_id='.$min.'>; rel="prev"'; + return response() + ->json($res) + ->withHeaders([ + 'Link' => $link, + ]); + } else { + return response()->json($res); + } } /** @@ -1098,6 +1302,17 @@ class ApiV1Controller extends Controller $path = $photo->store($storagePath); $hash = \hash_file('sha256', $photo); $license = null; + $mime = $photo->getMimeType(); + + // if($photo->getMimeType() == 'image/heic') { + // abort_if(config('image.driver') !== 'imagick', 422, 'Invalid media type'); + // abort_if(!in_array('HEIC', \Imagick::queryformats()), 422, 'Unsupported media type'); + // $oldPath = $path; + // $path = str_replace('.heic', '.jpg', $path); + // $mime = 'image/jpeg'; + // \Image::make($photo)->save(storage_path("app/{$path}")); + // @unlink(storage_path("app/{$oldPath}")); + // } $settings = UserSetting::whereUserId($user->id)->first(); @@ -1118,7 +1333,7 @@ class ApiV1Controller extends Controller $media->media_path = $path; $media->original_sha256 = $hash; $media->size = $photo->getSize(); - $media->mime = $photo->getMimeType(); + $media->mime = $mime; $media->caption = $request->input('description'); $media->filter_class = $filterClass; $media->filter_name = $filterName; @@ -1327,7 +1542,7 @@ class ApiV1Controller extends Controller NotificationService::warmCache($pid, 400, true); } - $baseUrl = config('app.url') . '/api/v1/notifications?'; + $baseUrl = config('app.url') . '/api/v1/notifications?limit=' . $limit . '&'; if($minId == $maxId) { $minId = null; @@ -1469,8 +1684,47 @@ class ApiV1Controller extends Controller public function conversations(Request $request) { abort_if(!$request->user(), 403); + $this->validate($request, [ + 'limit' => 'min:1|max:40', + 'scope' => 'nullable|in:inbox,sent,requests' + ]); - return response()->json([]); + $limit = $request->input('limit', 20); + $scope = $request->input('scope', 'inbox'); + $pid = $request->user()->profile_id; + + $dms = DirectMessage::when($scope === 'inbox', function($q, $scope) use($pid) { + return $q->whereIsHidden(false)->whereToId($pid)->orWhere('from_id', $pid)->groupBy('to_id'); + }) + ->when($scope === 'sent', function($q, $scope) use($pid) { + return $q->whereFromId($pid)->groupBy('to_id'); + }) + ->when($scope === 'requests', function($q, $scope) use($pid) { + return $q->whereToId($pid)->whereIsHidden(true); + }) + ->latest() + ->simplePaginate($limit) + ->map(function($dm) use($pid) { + $from = $pid == $dm->to_id ? $dm->from_id : $dm->to_id; + $res = [ + 'id' => $dm->id, + 'unread' => false, + 'accounts' => [ + AccountService::get($from) + ], + 'last_status' => StatusService::getDirectMessage($dm->status_id) + ]; + return $res; + }) + ->filter(function($dm) { + return isset($dm['accounts']) && count($dm['accounts']); + }) + ->unique(function($item, $key) { + return $item['accounts'][0]['id']; + }) + ->values(); + + return response()->json($dms); } /** @@ -1546,9 +1800,9 @@ class ApiV1Controller extends Controller } } - $resource = new Fractal\Resource\Item($status, new StatusTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - + $res = StatusService::get($status->id); + $res['favourited'] = LikeService::liked($user->profile_id, $status->id); + $res['reblogged'] = false; return response()->json($res); } @@ -1688,9 +1942,16 @@ class ApiV1Controller extends Controller 'limit' => 'nullable|integer|min:1|max:80' ]); + $page = $request->input('page', 1); $limit = $request->input('limit') ?? 40; $user = $request->user(); $status = Status::findOrFail($id); + $offset = $page == 1 ? 0 : ($page * $limit - $limit); + if($offset > 100) { + if($user->profile_id != $status->profile_id) { + return []; + } + } if($status->profile_id !== $user->profile_id) { if($status->scope == 'private') { @@ -1700,9 +1961,27 @@ class ApiV1Controller extends Controller } } - $liked = $status->likedBy()->latest()->simplePaginate($limit); - $resource = new Fractal\Resource\Collection($liked, new AccountTransformer()); - $res = $this->fractal->createData($resource)->toArray(); + $res = DB::table('likes') + ->select('likes.id', 'likes.profile_id', 'likes.status_id', 'followers.created_at') + ->leftJoin('followers', function($join) use($user, $status) { + return $join->on('likes.profile_id', '=', 'followers.following_id') + ->where('followers.profile_id', $user->profile_id) + ->where('likes.status_id', $status->id); + }) + ->whereStatusId($status->id) + ->orderByDesc('followers.created_at') + ->offset($offset) + ->limit($limit) + ->get() + ->map(function($like) { + $account = AccountService::get($like->profile_id); + $account['follows'] = isset($like->created_at); + return $account; + }) + ->filter(function($account) use($user) { + return $account && isset($account['id']) && $account['id'] != $user->profile_id; + }) + ->values(); $url = $request->url(); $page = $request->input('page', 1); @@ -1836,6 +2115,9 @@ class ApiV1Controller extends Controller } NewStatusPipeline::dispatch($status); + if($status->in_reply_to_id) { + CommentPipeline::dispatch($parent, $status); + } Cache::forget('user:account:id:'.$user->id); Cache::forget('_api:statuses:recent_9:'.$user->profile_id); Cache::forget('profile:status_count:'.$user->profile_id); @@ -2131,4 +2413,71 @@ class ApiV1Controller extends Controller return SearchApiV2Service::query($request); } + + /** + * GET /api/v1/discover/posts + * + * + * @return array + */ + public function discoverPosts(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'limit' => 'integer|min:1|max:40' + ]); + + $limit = $request->input('limit', 40); + $profile = Auth::user()->profile; + $pid = $profile->id; + + $following = Cache::remember('feature:discover:following:'.$pid, now()->addMinutes(15), function() use ($pid) { + return Follower::whereProfileId($pid)->pluck('following_id')->toArray(); + }); + + $filters = Cache::remember("user:filter:list:$pid", now()->addMinutes(15), function() use($pid) { + $private = Profile::whereIsPrivate(true) + ->orWhere('unlisted', true) + ->orWhere('status', '!=', null) + ->pluck('id') + ->toArray(); + $filters = UserFilter::whereUserId($pid) + ->whereFilterableType('App\Profile') + ->whereIn('filter_type', ['mute', 'block']) + ->pluck('filterable_id') + ->toArray(); + return array_merge($private, $filters); + }); + $following = array_merge($following, $filters); + + $sql = config('database.default') !== 'pgsql'; + $min_id = SnowflakeService::byDate(now()->subMonths(3)); + $res = Status::select( + 'id', + 'is_nsfw', + 'profile_id', + 'type', + 'uri', + ) + ->whereNull('uri') + ->whereIn('type', ['photo','photo:album', 'video']) + ->whereIsNsfw(false) + ->whereVisibility('public') + ->whereNotIn('profile_id', $following) + ->where('id', '>', $min_id) + ->inRandomOrder() + ->take($limit) + ->pluck('id') + ->map(function($post) { + return StatusService::get($post); + }) + ->filter(function($post) { + return $post && isset($post['id']); + }) + ->values() + ->toArray(); + + return response()->json($res); + } } diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php index d7f9867da..ee58ada6b 100644 --- a/app/Http/Controllers/Api/BaseApiController.php +++ b/app/Http/Controllers/Api/BaseApiController.php @@ -304,7 +304,7 @@ class BaseApiController extends Controller $status->scope = 'archived'; $status->visibility = 'draft'; $status->save(); - StatusService::del($status->id); + StatusService::del($status->id, true); AccountService::syncPostCount($status->profile_id); return [200]; @@ -331,7 +331,7 @@ class BaseApiController extends Controller $status->visibility = $archive->original_scope; $status->save(); $archive->delete(); - StatusService::del($status->id); + StatusService::del($status->id, true); AccountService::syncPostCount($status->profile_id); return [200]; diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index bdbe84559..42a5490d0 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -73,14 +73,11 @@ class CommentController extends Controller $reply->visibility = $scope; $reply->save(); - $status->reply_count++; - $status->save(); - return $reply; }); StatusService::del($status->id); - NewStatusPipeline::dispatch($reply, false); + NewStatusPipeline::dispatch($reply); CommentPipeline::dispatch($status, $reply); if ($request->ajax()) { @@ -89,11 +86,11 @@ class CommentController extends Controller $entity = new Fractal\Resource\Item($reply, new StatusTransformer()); $entity = $fractal->createData($entity)->toArray(); $response = [ - 'code' => 200, - 'msg' => 'Comment saved', - 'username' => $profile->username, - 'url' => $reply->url(), - 'profile' => $profile->url(), + 'code' => 200, + 'msg' => 'Comment saved', + 'username' => $profile->username, + 'url' => $reply->url(), + 'profile' => $profile->url(), 'comment' => $reply->caption, 'entity' => $entity, ]; diff --git a/app/Http/Controllers/InternalApiController.php b/app/Http/Controllers/InternalApiController.php index b9f86a639..6f0cbfc6a 100644 --- a/app/Http/Controllers/InternalApiController.php +++ b/app/Http/Controllers/InternalApiController.php @@ -17,6 +17,7 @@ use App\{ Profile, StatusHashtag, Status, + User, UserFilter, }; use Auth,Cache; @@ -194,9 +195,12 @@ class InternalApiController extends Controller $item_id = $request->input('item_id'); $item_type = $request->input('item_type'); + $status = Status::findOrFail($item_id); + $author = User::whereProfileId($status->profile_id)->first(); + abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts'); + switch($action) { case 'addcw': - $status = Status::findOrFail($item_id); $status->is_nsfw = true; $status->save(); ModLogService::boot() @@ -212,7 +216,6 @@ class InternalApiController extends Controller ->accessLevel('admin') ->save(); - if($status->uri == null) { $media = $status->media; $ai = new AccountInterstitial; @@ -243,7 +246,6 @@ class InternalApiController extends Controller break; case 'remcw': - $status = Status::findOrFail($item_id); $status->is_nsfw = false; $status->save(); ModLogService::boot() @@ -269,7 +271,6 @@ class InternalApiController extends Controller break; case 'unlist': - $status = Status::whereScope('public')->findOrFail($item_id); $status->scope = $status->visibility = 'unlisted'; $status->save(); PublicTimelineService::del($status->id); @@ -316,7 +317,6 @@ class InternalApiController extends Controller break; case 'spammer': - $status = Status::findOrFail($item_id); HandleSpammerPipeline::dispatch($status->profile); ModLogService::boot() ->user(Auth::user()) @@ -333,10 +333,7 @@ class InternalApiController extends Controller break; } - Cache::forget('_api:statuses:recent_9:' . $status->profile_id); - Cache::forget('profile:embed:' . $status->profile_id); - StatusService::del($status->id); - + StatusService::del($status->id, true); return ['msg' => 200]; } diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index d76a42171..07a6a6fa4 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -27,6 +27,7 @@ use App\Transformer\Api\{ }; use App\Services\{ AccountService, + FollowerService, LikeService, PublicTimelineService, ProfileService, @@ -151,13 +152,8 @@ class PublicApiController extends Controller if(Auth::check()) { $p = Auth::user()->profile; - $filtered = UserFilter::whereUserId($p->id) - ->whereFilterableType('App\Profile') - ->whereIn('filter_type', ['mute', 'block']) - ->pluck('filterable_id')->toArray(); - $scope = $p->id == $status->profile_id ? ['public', 'private', 'unlisted'] : ['public','unlisted']; + $scope = $p->id == $status->profile_id || FollowerService::follows($p->id, $profile->id) ? ['public', 'private', 'unlisted'] : ['public','unlisted']; } else { - $filtered = []; $scope = ['public', 'unlisted']; } @@ -166,7 +162,6 @@ class PublicApiController extends Controller $replies = $status->comments() ->whereNull('reblog_of_id') ->whereIn('scope', $scope) - ->whereNotIn('profile_id', $filtered) ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->where('id', '>=', $request->min_id) ->orderBy('id', 'desc') @@ -176,17 +171,15 @@ class PublicApiController extends Controller $replies = $status->comments() ->whereNull('reblog_of_id') ->whereIn('scope', $scope) - ->whereNotIn('profile_id', $filtered) ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->where('id', '<=', $request->max_id) ->orderBy('id', 'desc') ->paginate($limit); } } else { - $replies = $status->comments() + $replies = Status::whereInReplyToId($status->id) ->whereNull('reblog_of_id') ->whereIn('scope', $scope) - ->whereNotIn('profile_id', $filtered) ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->orderBy('id', 'desc') ->paginate($limit); @@ -290,100 +283,100 @@ class PublicApiController extends Controller $filtered = $user ? UserFilterService::filters($user->profile_id) : []; if(config('exp.cached_public_timeline') == false) { - 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']) - ->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; - }) - ->filter(function($s) use($filtered) { - return in_array($s['account']['id'], $filtered) == false; - }); - $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']) - ->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; - }) - ->filter(function($s) use($filtered) { - return in_array($s['account']['id'], $filtered) == false; - }); + 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']) + ->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; + }) + ->filter(function($s) use($filtered) { + return in_array($s['account']['id'], $filtered) == false; + }); + $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']) + ->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; + }) + ->filter(function($s) use($filtered) { + return in_array($s['account']['id'], $filtered) == false; + }); - $res = $timeline->toArray(); - } + $res = $timeline->toArray(); + } } else { - Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { - if(PublicTimelineService::count() == 0) { - PublicTimelineService::warmCache(true, 400); - } - }); + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { + if(PublicTimelineService::count() == 0) { + PublicTimelineService::warmCache(true, 400); + } + }); - if ($max) { - $feed = PublicTimelineService::getRankedMaxId($max, $limit); - } else if ($min) { - $feed = PublicTimelineService::getRankedMinId($min, $limit); - } else { - $feed = PublicTimelineService::get(0, $limit); - } + if ($max) { + $feed = PublicTimelineService::getRankedMaxId($max, $limit); + } else if ($min) { + $feed = PublicTimelineService::getRankedMinId($min, $limit); + } else { + $feed = PublicTimelineService::get(0, $limit); + } - $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; - }) - ->filter(function($s) use($filtered) { - return in_array($s['account']['id'], $filtered) == false; - }) - ->values() - ->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; + }) + ->filter(function($s) use($filtered) { + return in_array($s['account']['id'], $filtered) == false; + }) + ->values() + ->toArray(); } return response()->json($res); @@ -438,6 +431,7 @@ class PublicApiController extends Controller $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']; $textOnlyReplies = false; @@ -625,7 +619,7 @@ class PublicApiController extends Controller return $v != $pid; }) ->map(function($id) use($pid) { - return RelationshipService::get($pid, $id); + return RelationshipService::get($pid, $id); }); return response()->json($res); diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index f4b5f74cc..7d6099a28 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -11,9 +11,11 @@ use App\AccountInterstitial; use App\Media; use App\Profile; use App\Status; +use App\StatusArchived; use App\StatusView; use App\Transformer\ActivityPub\StatusTransformer; use App\Transformer\ActivityPub\Verb\Note; +use App\Transformer\ActivityPub\Verb\Question; use App\User; use Auth, DB, Cache; use Illuminate\Http\Request; @@ -81,16 +83,7 @@ class StatusController extends Controller public function shortcodeRedirect(Request $request, $id) { - abort_if(strlen($id) < 5, 404); - if(!Auth::check()) { - return redirect('/login?next='.urlencode('/' . $request->path())); - } - $id = HashidService::decode($id); - $status = Status::find($id); - if(!$status) { - return redirect('/404'); - } - return redirect($status->url()); + abort(404); } public function showId(int $id) @@ -215,7 +208,7 @@ class StatusController extends Controller Cache::forget('_api:statuses:recent_9:' . $status->profile_id); Cache::forget('profile:status_count:' . $status->profile_id); Cache::forget('profile:embed:' . $status->profile_id); - StatusService::del($status->id); + StatusService::del($status->id, true); if ($status->profile_id == $user->profile->id || $user->is_admin == true) { Cache::forget('profile:status_count:'.$status->profile_id); StatusDelete::dispatch($status); @@ -278,8 +271,9 @@ class StatusController extends Controller public function showActivityPub(Request $request, $status) { + $object = $status->type == 'poll' ? new Question() : new Note(); $fractal = new Fractal\Manager(); - $resource = new Fractal\Resource\Item($status, new Note()); + $resource = new Fractal\Resource\Item($status, $object); $res = $fractal->createData($resource)->toArray(); return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); diff --git a/app/Http/Controllers/StoryController.php b/app/Http/Controllers/StoryController.php index e35595a48..1c8890d87 100644 --- a/app/Http/Controllers/StoryController.php +++ b/app/Http/Controllers/StoryController.php @@ -68,6 +68,11 @@ class StoryController extends StoryComposeController 'avatar' => $profile['avatar'], 'local' => $profile['local'], 'username' => $profile['acct'], + 'latest' => [ + 'id' => $s->id, + 'type' => $s->type, + 'preview_url' => url(Storage::url($s->path)) + ], 'url' => $url, 'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)), 'sid' => $s->id diff --git a/app/Jobs/CommentPipeline/CommentPipeline.php b/app/Jobs/CommentPipeline/CommentPipeline.php index 0ca74e78b..d99cd6513 100644 --- a/app/Jobs/CommentPipeline/CommentPipeline.php +++ b/app/Jobs/CommentPipeline/CommentPipeline.php @@ -8,6 +8,7 @@ use App\{ UserFilter }; use App\Services\NotificationService; +use App\Services\StatusService; use DB, Cache, Log; use Illuminate\Support\Facades\Redis; @@ -58,6 +59,11 @@ class CommentPipeline implements ShouldQueue $target = $status->profile; $actor = $comment->profile; + DB::transaction(function() use($status) { + $status->reply_count = DB::table('statuses')->whereInReplyToId($status->id)->count(); + $status->save(); + }); + if ($actor->id === $target->id || $status->comments_disabled == true) { return true; } @@ -85,6 +91,7 @@ class CommentPipeline implements ShouldQueue NotificationService::setNotification($notification); NotificationService::set($notification->profile_id, $notification->id); + StatusService::del($status->id); }); } } diff --git a/app/Jobs/LikePipeline/LikePipeline.php b/app/Jobs/LikePipeline/LikePipeline.php index ce70790ca..d2be251ac 100644 --- a/app/Jobs/LikePipeline/LikePipeline.php +++ b/app/Jobs/LikePipeline/LikePipeline.php @@ -2,7 +2,7 @@ namespace App\Jobs\LikePipeline; -use Cache, Log; +use Cache, DB, Log; use Illuminate\Support\Facades\Redis; use App\{Like, Notification}; use Illuminate\Bus\Queueable; @@ -59,6 +59,9 @@ class LikePipeline implements ShouldQueue return; } + $status->likes_count = DB::table('likes')->whereStatusId($status->id)->count(); + $status->save(); + StatusService::refresh($status->id); if($status->url && $actor->domain == null) { diff --git a/app/Jobs/LikePipeline/UnlikePipeline.php b/app/Jobs/LikePipeline/UnlikePipeline.php index 0e3ff4785..267332974 100644 --- a/app/Jobs/LikePipeline/UnlikePipeline.php +++ b/app/Jobs/LikePipeline/UnlikePipeline.php @@ -2,7 +2,7 @@ namespace App\Jobs\LikePipeline; -use Cache, Log; +use Cache, DB, Log; use Illuminate\Support\Facades\Redis; use App\{Like, Notification}; use Illuminate\Bus\Queueable; @@ -59,9 +59,8 @@ class UnlikePipeline implements ShouldQueue return; } - $count = $status->likes_count > 1 ? $status->likes_count : $status->likes()->count(); - $status->likes_count = $count - 1; - $status->save(); + $status->likes_count = DB::table('likes')->whereStatusId($status->id)->count(); + $status->save(); StatusService::refresh($status->id); diff --git a/app/Jobs/ModPipeline/HandleSpammerPipeline.php b/app/Jobs/ModPipeline/HandleSpammerPipeline.php index 0e5b4042d..33992f7e7 100644 --- a/app/Jobs/ModPipeline/HandleSpammerPipeline.php +++ b/app/Jobs/ModPipeline/HandleSpammerPipeline.php @@ -41,7 +41,7 @@ class HandleSpammerPipeline implements ShouldQueue $status->scope = $status->scope === 'public' ? 'unlisted' : $status->scope; $status->visibility = $status->scope; $status->save(); - StatusService::del($status->id); + StatusService::del($status->id, true); } }); diff --git a/app/Jobs/SharePipeline/SharePipeline.php b/app/Jobs/SharePipeline/SharePipeline.php index db7ea4c01..8cccf8406 100644 --- a/app/Jobs/SharePipeline/SharePipeline.php +++ b/app/Jobs/SharePipeline/SharePipeline.php @@ -15,6 +15,7 @@ use League\Fractal\Serializer\ArraySerializer; use App\Transformer\ActivityPub\Verb\Announce; use GuzzleHttp\{Pool, Client, Promise}; use App\Util\ActivityPub\HttpSignature; +use App\Services\StatusService; class SharePipeline implements ShouldQueue { @@ -76,6 +77,7 @@ class SharePipeline implements ShouldQueue $parent->reblogs_count = $parent->shares()->count(); $parent->save(); + StatusService::del($parent); try { $notification = new Notification; diff --git a/app/Jobs/SharePipeline/UndoSharePipeline.php b/app/Jobs/SharePipeline/UndoSharePipeline.php index 6336ac675..ba3a2791b 100644 --- a/app/Jobs/SharePipeline/UndoSharePipeline.php +++ b/app/Jobs/SharePipeline/UndoSharePipeline.php @@ -56,7 +56,7 @@ class UndoSharePipeline implements ShouldQueue StatusService::del($parent->id); } - $status->delete(); + $status->forceDelete(); return 1; } diff --git a/app/Services/AccountService.php b/app/Services/AccountService.php index 7042b3c94..b57361069 100644 --- a/app/Services/AccountService.php +++ b/app/Services/AccountService.php @@ -5,6 +5,7 @@ namespace App\Services; use Cache; use App\Profile; use App\Status; +use App\UserSetting; use App\Transformer\Api\AccountTransformer; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; @@ -17,14 +18,7 @@ class AccountService public static function get($id, $softFail = false) { - if($id > PHP_INT_MAX || $id < 1) { - return []; - } - - $key = self::CACHE_KEY . $id; - $ttl = now()->addHours(12); - - return Cache::remember($key, $ttl, function() use($id, $softFail) { + return Cache::remember(self::CACHE_KEY . $id, 43200, function() use($id, $softFail) { $fractal = new Fractal\Manager(); $fractal->setSerializer(new ArraySerializer()); $profile = Profile::find($id); @@ -44,6 +38,63 @@ class AccountService return Cache::forget(self::CACHE_KEY . $id); } + public static function settings($id) + { + $settings = UserSetting::whereUserId($id)->first(); + if(!$settings) { + return self::defaultSettings(); + } + return collect($settings) + ->filter(function($item, $key) { + return in_array($key, array_keys(self::defaultSettings())) == true; + }) + ->map(function($item, $key) { + if($key == 'compose_settings') { + $cs = self::defaultSettings()['compose_settings']; + return array_merge($cs, $item ?? []); + } + + if($key == 'other') { + $other = self::defaultSettings()['other']; + return array_merge($other, $item ?? []); + } + return $item; + }); + } + + public static function canEmbed($id) + { + return self::settings($id)['other']['disable_embeds'] == false; + } + + public static function defaultSettings() + { + return [ + 'crawlable' => true, + 'public_dm' => false, + 'reduce_motion' => false, + 'high_contrast_mode' => false, + 'video_autoplay' => false, + 'show_profile_follower_count' => true, + 'show_profile_following_count' => true, + 'compose_settings' => [ + 'default_scope' => 'public', + 'default_license' => 1, + 'media_descriptions' => false + ], + 'other' => [ + 'advanced_atom' => false, + 'disable_embeds' => false, + 'mutual_mention_notifications' => false, + 'hide_collections' => false, + 'hide_like_counts' => false, + 'hide_groups' => false, + 'hide_stories' => false, + 'disable_cw' => false, + ] + ]; + } + public static function syncPostCount($id) { $profile = Profile::find($id); diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index a96773cae..3ea3d8ddc 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -4,6 +4,7 @@ namespace App\Services; use Illuminate\Support\Facades\Redis; use Cache; +use DB; use App\{ Follower, Profile, @@ -12,6 +13,7 @@ use App\{ class FollowerService { + const CACHE_KEY = 'pf:services:followers:'; const FOLLOWING_KEY = 'pf:services:follow:following:id:'; const FOLLOWERS_KEY = 'pf:services:follow:followers:id:'; @@ -87,4 +89,29 @@ class FollowerService }); } + 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(); + }); + } + } diff --git a/app/Services/HashtagService.php b/app/Services/HashtagService.php new file mode 100644 index 000000000..6a40595d3 --- /dev/null +++ b/app/Services/HashtagService.php @@ -0,0 +1,32 @@ + $tag->name, + 'slug' => $tag->slug, + ]; + }); + } + + public static function count($id) + { + return Cache::remember('services:hashtag:count:by_id:' . $id, 3600, function() use($id) { + return StatusHashtag::whereHashtagId($id)->count(); + }); + } + +} diff --git a/app/Services/MediaService.php b/app/Services/MediaService.php index f6cbd87f4..42f188eba 100644 --- a/app/Services/MediaService.php +++ b/app/Services/MediaService.php @@ -19,6 +19,9 @@ class MediaService public static function get($statusId) { $status = Status::find($statusId); + if(!$status) { + return []; + } $ttl = $status->created_at->lt(now()->subMinutes(30)) ? 129600 : 30; return Cache::remember(self::CACHE_KEY.$statusId, $ttl, function() use($status) { if(!$status) { diff --git a/app/Services/MediaStorageService.php b/app/Services/MediaStorageService.php index 4baa7cdf8..89d7ccdaa 100644 --- a/app/Services/MediaStorageService.php +++ b/app/Services/MediaStorageService.php @@ -43,18 +43,24 @@ class MediaStorageService { $h = $r->getHeaders(); - if (isset($h['Content-Length'], $h['Content-Type']) == false || - empty($h['Content-Length']) || - empty($h['Content-Type']) || - $h['Content-Length'] < 10 || - $h['Content-Length'] > (config_cache('pixelfed.max_photo_size') * 1000) - ) { + if (isset($h['Content-Length'], $h['Content-Type']) == false) { + return false; + } + + if(empty($h['Content-Length']) || empty($h['Content-Type']) ) { + return false; + } + + $len = is_array($h['Content-Length']) ? $h['Content-Length'][0] : $h['Content-Length']; + $mime = is_array($h['Content-Type']) ? $h['Content-Type'][0] : $h['Content-Type']; + + if($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) { return false; } return [ - 'length' => $h['Content-Length'][0], - 'mime' => $h['Content-Type'][0] + 'length' => $len, + 'mime' => $mime ]; } diff --git a/app/Services/SearchApiV2Service.php b/app/Services/SearchApiV2Service.php index 7fc7c6260..f3633481c 100644 --- a/app/Services/SearchApiV2Service.php +++ b/app/Services/SearchApiV2Service.php @@ -12,6 +12,9 @@ use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Util\ActivityPub\Helpers; use Illuminate\Support\Str; +use App\Services\AccountService; +use App\Services\HashtagService; +use App\Services\StatusService; class SearchApiV2Service { @@ -86,19 +89,27 @@ class SearchApiV2Service protected function accounts() { + $user = request()->user(); $limit = $this->query->input('limit') ?? 20; $offset = $this->query->input('offset') ?? 0; $query = '%' . $this->query->input('q') . '%'; - $results = Profile::whereNull('status') + $results = Profile::select('profiles.*', 'followers.profile_id', 'followers.created_at') + ->whereNull('status') + ->leftJoin('followers', function($join) use($user) { + return $join->on('profiles.id', '=', 'followers.following_id') + ->where('followers.profile_id', $user->profile_id); + }) ->where('username', 'like', $query) + ->orderByDesc('profiles.followers_count') + ->orderByDesc('followers.created_at') ->offset($offset) ->limit($limit) - ->get(); + ->get() + ->map(function($res) { + return AccountService::get($res['id']); + }); - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Collection($results, new AccountTransformer()); - return $fractal->createData($resource)->toArray(); + return $results; } protected function hashtags() @@ -115,6 +126,7 @@ class SearchApiV2Service return [ 'name' => $tag->name, 'url' => $tag->url(), + 'count' => HashtagService::count($tag->id), 'history' => [] ]; }); @@ -134,12 +146,11 @@ class SearchApiV2Service $results = Status::where('caption', 'like', $query) ->whereProfileId($accountId) ->limit($limit) - ->get(); - - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Collection($results, new StatusTransformer()); - return $fractal->createData($resource)->toArray(); + ->get() + ->map(function($status) { + return StatusService::get($status->id); + }); + return $results; } protected function resolveQuery() @@ -213,4 +224,4 @@ class SearchApiV2Service ]; } -} \ No newline at end of file +} diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index a4cfc4b75..c13ddd58b 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -4,6 +4,7 @@ namespace App\Services; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Redis; +use DB; use App\Status; //use App\Transformer\Api\v3\StatusTransformer; use App\Transformer\Api\StatusStatelessTransformer; @@ -12,8 +13,8 @@ use League\Fractal; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; -class StatusService { - +class StatusService +{ const CACHE_KEY = 'pf:services:status:'; public static function key($id, $publicOnly = true) @@ -47,18 +48,36 @@ class StatusService { return $res; } - public static function del($id) + public static function getDirectMessage($id) + { + $status = Status::whereScope('direct')->find($id); + + if(!$status) { + return null; + } + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($status, new StatusTransformer()); + return $fractal->createData($resource)->toArray(); + } + + public static function del($id, $purge = false) { $status = self::get($id); - if($status && isset($status['account']) && isset($status['account']['id'])) { - Cache::forget('profile:embed:' . $status['account']['id']); + + if($purge) { + if($status && isset($status['account']) && isset($status['account']['id'])) { + Cache::forget('profile:embed:' . $status['account']['id']); + } + Cache::forget('status:transformer:media:attachments:' . $id); + MediaService::del($id); + Cache::forget('status:thumb:nsfw0' . $id); + Cache::forget('status:thumb:nsfw1' . $id); + Cache::forget('pf:services:sh:id:' . $id); + PublicTimelineService::rem($id); } - Cache::forget('status:transformer:media:attachments:' . $id); - MediaService::del($id); - Cache::forget('status:thumb:nsfw0' . $id); - Cache::forget('status:thumb:nsfw1' . $id); - Cache::forget('pf:services:sh:id:' . $id); - PublicTimelineService::rem($id); + Cache::forget(self::key($id, false)); return Cache::forget(self::key($id)); } diff --git a/app/Transformer/Api/AccountTransformer.php b/app/Transformer/Api/AccountTransformer.php index 4b8e16976..8beb5b668 100644 --- a/app/Transformer/Api/AccountTransformer.php +++ b/app/Transformer/Api/AccountTransformer.php @@ -29,6 +29,7 @@ class AccountTransformer extends Fractal\TransformerAbstract 'following_count' => $profile->followingCount(), 'statuses_count' => (int) $profile->statusCount(), 'note' => $profile->bio ?? '', + 'note_text' => strip_tags($profile->bio), 'url' => $profile->url(), 'avatar' => $profile->avatarUrl(), 'website' => $profile->website, @@ -37,7 +38,8 @@ class AccountTransformer extends Fractal\TransformerAbstract 'created_at' => $profile->created_at->toJSON(), 'header_bg' => $profile->header_bg, 'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(), - 'pronouns' => PronounService::get($profile->id) + 'pronouns' => PronounService::get($profile->id), + 'location' => $profile->location ]; } diff --git a/app/UserSetting.php b/app/UserSetting.php index a8c67739d..41e12dd84 100644 --- a/app/UserSetting.php +++ b/app/UserSetting.php @@ -6,5 +6,10 @@ use Illuminate\Database\Eloquent\Model; class UserSetting extends Model { - protected $fillable = ['user_id']; + protected $fillable = ['user_id']; + + protected $casts = [ + 'compose_settings' => 'json', + 'other' => 'json' + ]; } diff --git a/app/Util/Sentiment/Bouncer.php b/app/Util/Sentiment/Bouncer.php index 4e1983334..45e002952 100644 --- a/app/Util/Sentiment/Bouncer.php +++ b/app/Util/Sentiment/Bouncer.php @@ -55,7 +55,6 @@ class Bouncer { } if( $status->profile->created_at->gt(now()->subMonths(6)) && - $status->profile->status_count < 2 && $status->profile->bio && $status->profile->website ) { diff --git a/database/migrations/2021_11_06_100552_add_more_settings_to_user_settings_table.php b/database/migrations/2021_11_06_100552_add_more_settings_to_user_settings_table.php new file mode 100644 index 000000000..00af978f2 --- /dev/null +++ b/database/migrations/2021_11_06_100552_add_more_settings_to_user_settings_table.php @@ -0,0 +1,32 @@ +json('other')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('user_settings', function (Blueprint $table) { + $table->dropColumn('other'); + }); + } +}