Pārlūkot izejas kodu

Update ApiV1Controller, improve settings and add discoverPosts endpoint

Daniel Supernault 3 gadi atpakaļ
vecāks
revīzija
079804e65b
1 mainītis faili ar 443 papildinājumiem un 98 dzēšanām
  1. 443 98
      app/Http/Controllers/Api/ApiV1Controller.php

+ 443 - 98
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,8 @@ 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\LikePipeline\LikePipeline;
 use App\Jobs\SharePipeline\SharePipeline;
 use App\Jobs\StatusPipeline\NewStatusPipeline;
@@ -49,8 +53,11 @@ use App\Jobs\VideoPipeline\{
 	VideoPostProcess,
 	VideoThumbnail
 };
+
 use App\Services\{
+	AccountService,
 	LikeService,
+	InstanceService,
 	NotificationService,
 	MediaPathService,
 	PublicTimelineService,
@@ -59,9 +66,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 +177,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;
-
-		if($displayName !== $user->name) {
-			$user->name = $displayName;
-			$profile->name = $displayName;
+		$other = array_merge(AccountService::defaultSettings()['other'], $settings->other ?? []);
+		$syncLicenses = false;
+		$licenseChanged = false;
+		$composeSettings = array_merge(AccountService::defaultSettings()['compose_settings'], $settings->compose_settings ?? []);
+
+		// 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 +491,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 +504,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 +515,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();
-		}
-
-		$resource = new Fractal\Resource\Collection($timeline, new StatusTransformer());
-		$res = $this->fractal->createData($resource)->toArray();
+		$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();
+
 		return response()->json($res);
 	}
 
@@ -417,6 +585,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 +604,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 +638,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 +651,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 +673,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 +939,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 +1301,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 +1332,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 +1541,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 +1683,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);
 	}
 
 	/**
@@ -1688,9 +1941,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 +1960,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);
@@ -2131,4 +2409,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);
+	}
 }