From 18382e8a1f55775095ef989ec99bf01023b6fe62 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 7 Mar 2024 02:38:50 -0700 Subject: [PATCH] Update DiscoverController, handle discover hashtag redirects --- app/Http/Controllers/DiscoverController.php | 653 ++++++++++---------- 1 file changed, 333 insertions(+), 320 deletions(-) diff --git a/app/Http/Controllers/DiscoverController.php b/app/Http/Controllers/DiscoverController.php index 4bb7277a4..862a16ad0 100644 --- a/app/Http/Controllers/DiscoverController.php +++ b/app/Http/Controllers/DiscoverController.php @@ -2,366 +2,379 @@ namespace App\Http\Controllers; -use App\{ - DiscoverCategory, - Follower, - Hashtag, - HashtagFollow, - Instance, - Like, - Profile, - Status, - StatusHashtag, - UserFilter -}; -use Auth, DB, Cache; -use Illuminate\Http\Request; +use App\Hashtag; +use App\Instance; +use App\Like; use App\Services\BookmarkService; use App\Services\ConfigCacheService; use App\Services\HashtagService; use App\Services\LikeService; use App\Services\ReblogService; -use App\Services\StatusHashtagService; use App\Services\SnowflakeService; +use App\Services\StatusHashtagService; use App\Services\StatusService; use App\Services\TrendingHashtagService; use App\Services\UserFilterService; +use App\Status; +use Auth; +use Cache; +use DB; +use Illuminate\Http\Request; class DiscoverController extends Controller { - public function home(Request $request) - { - abort_if(!Auth::check() && config('instance.discover.public') == false, 403); - return view('discover.home'); - } + public function home(Request $request) + { + abort_if(! Auth::check() && config('instance.discover.public') == false, 403); - public function showTags(Request $request, $hashtag) - { - abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403); + return view('discover.home'); + } - $tag = Hashtag::whereName($hashtag) - ->orWhere('slug', $hashtag) - ->where('is_banned', '!=', true) - ->firstOrFail(); - $tagCount = StatusHashtagService::count($tag->id); - return view('discover.tags.show', compact('tag', 'tagCount')); - } + public function showTags(Request $request, $hashtag) + { + if ($request->user()) { + return redirect('/i/web/hashtag/'.$hashtag.'?src=pd'); + } + abort_if(! config('instance.discover.tags.is_public') && ! Auth::check(), 403); - public function getHashtags(Request $request) - { - $user = $request->user(); - abort_if(!config('instance.discover.tags.is_public') && !$user, 403); + $tag = Hashtag::whereName($hashtag) + ->orWhere('slug', $hashtag) + ->where('is_banned', '!=', true) + ->firstOrFail(); + $tagCount = $tag->cached_count ?? 0; - $this->validate($request, [ - 'hashtag' => 'required|string|min:1|max:124', - 'page' => 'nullable|integer|min:1|max:' . ($user ? 29 : 3) - ]); + return view('discover.tags.show', compact('tag', 'tagCount')); + } - $page = $request->input('page') ?? '1'; - $end = $page > 1 ? $page * 9 : 0; - $tag = $request->input('hashtag'); + public function getHashtags(Request $request) + { + $user = $request->user(); + abort_if(! config('instance.discover.tags.is_public') && ! $user, 403); - if(config('database.default') === 'pgsql') { - $hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail(); - } else { - $hashtag = Hashtag::whereName($tag)->firstOrFail(); - } + $this->validate($request, [ + 'hashtag' => 'required|string|min:1|max:124', + 'page' => 'nullable|integer|min:1|max:'.($user ? 29 : 3), + ]); - if($hashtag->is_banned == true) { - return []; - } - if($user) { - $res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id); - } - $res['hashtag'] = [ - 'name' => $hashtag->name, - 'url' => $hashtag->url() - ]; - if($user) { - $tags = StatusHashtagService::get($hashtag->id, $page, $end); - $res['tags'] = collect($tags) - ->map(function($tag) use($user) { - $tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']); - $tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']); - $tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']); - return $tag; - }) - ->filter(function($tag) { - if(!StatusService::get($tag['status']['id'])) { - return false; - } - return true; - }) - ->values(); - } else { - if($page != 1) { - $res['tags'] = []; - return $res; - } - $key = 'discover:tags:public_feed:' . $hashtag->id . ':page:' . $page; - $tags = Cache::remember($key, 43200, function() use($hashtag, $page, $end) { - return collect(StatusHashtagService::get($hashtag->id, $page, $end)) - ->filter(function($tag) { - if(!$tag['status']['local']) { - return false; - } - return true; - }) - ->values(); - }); - $res['tags'] = collect($tags) - ->filter(function($tag) { - if(!StatusService::get($tag['status']['id'])) { - return false; - } - return true; - }) - ->values(); - } - return $res; - } + $page = $request->input('page') ?? '1'; + $end = $page > 1 ? $page * 9 : 0; + $tag = $request->input('hashtag'); - public function profilesDirectory(Request $request) - { - return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.'); - } + if (config('database.default') === 'pgsql') { + $hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail(); + } else { + $hashtag = Hashtag::whereName($tag)->firstOrFail(); + } - public function profilesDirectoryApi(Request $request) - { - return ['error' => 'Temporarily unavailable.']; - } + if ($hashtag->is_banned == true) { + return []; + } + if ($user) { + $res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id); + } + $res['hashtag'] = [ + 'name' => $hashtag->name, + 'url' => $hashtag->url(), + ]; + if ($user) { + $tags = StatusHashtagService::get($hashtag->id, $page, $end); + $res['tags'] = collect($tags) + ->map(function ($tag) use ($user) { + $tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']); + $tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']); + $tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']); - public function trendingApi(Request $request) - { - abort_if(config('instance.discover.public') == false && !$request->user(), 403); + return $tag; + }) + ->filter(function ($tag) { + if (! StatusService::get($tag['status']['id'])) { + return false; + } - $this->validate($request, [ - 'range' => 'nullable|string|in:daily,monthly,yearly', - ]); + return true; + }) + ->values(); + } else { + if ($page != 1) { + $res['tags'] = []; - $range = $request->input('range'); - $days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365); - $ttls = [ - 1 => 1500, - 31 => 14400, - 365 => 86400 - ]; - $key = ':api:discover:trending:v2.12:range:' . $days; + return $res; + } + $key = 'discover:tags:public_feed:'.$hashtag->id.':page:'.$page; + $tags = Cache::remember($key, 43200, function () use ($hashtag, $page, $end) { + return collect(StatusHashtagService::get($hashtag->id, $page, $end)) + ->filter(function ($tag) { + if (! $tag['status']['local']) { + return false; + } - $ids = Cache::remember($key, $ttls[$days], function() use($days) { - $min_id = SnowflakeService::byDate(now()->subDays($days)); - return DB::table('statuses') - ->select( - 'id', - 'scope', - 'type', - 'is_nsfw', - 'likes_count', - 'created_at' - ) - ->where('id', '>', $min_id) - ->whereNull('uri') - ->whereScope('public') - ->whereIn('type', [ - 'photo', - 'photo:album', - 'video' - ]) - ->whereIsNsfw(false) - ->orderBy('likes_count','desc') - ->take(30) - ->pluck('id'); - }); + return true; + }) + ->values(); + }); + $res['tags'] = collect($tags) + ->filter(function ($tag) { + if (! StatusService::get($tag['status']['id'])) { + return false; + } - $filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : []; + return true; + }) + ->values(); + } - $res = $ids->map(function($s) { - return StatusService::get($s); - })->filter(function($s) use($filtered) { - return - $s && - !in_array($s['account']['id'], $filtered) && - isset($s['account']); - })->values(); + return $res; + } - return response()->json($res); - } + public function profilesDirectory(Request $request) + { + return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.'); + } - public function trendingHashtags(Request $request) - { - abort_if(!$request->user(), 403); + public function profilesDirectoryApi(Request $request) + { + return ['error' => 'Temporarily unavailable.']; + } - $res = TrendingHashtagService::getTrending(); - return $res; - } + public function trendingApi(Request $request) + { + abort_if(config('instance.discover.public') == false && ! $request->user(), 403); - public function trendingPlaces(Request $request) - { - return []; - } + $this->validate($request, [ + 'range' => 'nullable|string|in:daily,monthly,yearly', + ]); - public function myMemories(Request $request) - { - abort_if(!$request->user(), 404); - $pid = $request->user()->profile_id; - abort_if(!$this->config()['memories']['enabled'], 404); - $type = $request->input('type') ?? 'posts'; + $range = $request->input('range'); + $days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365); + $ttls = [ + 1 => 1500, + 31 => 14400, + 365 => 86400, + ]; + $key = ':api:discover:trending:v2.12:range:'.$days; - switch($type) { - case 'posts': - $res = Status::whereProfileId($pid) - ->whereDay('created_at', date('d')) - ->whereMonth('created_at', date('m')) - ->whereYear('created_at', '!=', date('Y')) - ->whereNull(['reblog_of_id', 'in_reply_to_id']) - ->limit(20) - ->pluck('id') - ->map(function($id) { - return StatusService::get($id, false); - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); - break; + $ids = Cache::remember($key, $ttls[$days], function () use ($days) { + $min_id = SnowflakeService::byDate(now()->subDays($days)); - case 'liked': - $res = Like::whereProfileId($pid) - ->whereDay('created_at', date('d')) - ->whereMonth('created_at', date('m')) - ->whereYear('created_at', '!=', date('Y')) - ->orderByDesc('status_id') - ->limit(20) - ->pluck('status_id') - ->map(function($id) { - $status = StatusService::get($id, false); - $status['favourited'] = true; - return $status; - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); - break; - } + return DB::table('statuses') + ->select( + 'id', + 'scope', + 'type', + 'is_nsfw', + 'likes_count', + 'created_at' + ) + ->where('id', '>', $min_id) + ->whereNull('uri') + ->whereScope('public') + ->whereIn('type', [ + 'photo', + 'photo:album', + 'video', + ]) + ->whereIsNsfw(false) + ->orderBy('likes_count', 'desc') + ->take(30) + ->pluck('id'); + }); - return $res; - } + $filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : []; - public function accountInsightsPopularPosts(Request $request) - { - abort_if(!$request->user(), 404); - $pid = $request->user()->profile_id; - abort_if(!$this->config()['insights']['enabled'], 404); - $posts = Cache::remember('pf:discover:metro2:accinsights:popular:' . $pid, 43200, function() use ($pid) { - return Status::whereProfileId($pid) - ->whereNotNull('likes_count') - ->orderByDesc('likes_count') - ->limit(12) - ->pluck('id') - ->map(function($id) { - return StatusService::get($id, false); - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); - }); + $res = $ids->map(function ($s) { + return StatusService::get($s); + })->filter(function ($s) use ($filtered) { + return + $s && + ! in_array($s['account']['id'], $filtered) && + isset($s['account']); + })->values(); - return $posts; - } + return response()->json($res); + } - public function config() - { - $cc = ConfigCacheService::get('config.discover.features'); - if($cc) { - return is_string($cc) ? json_decode($cc, true) : $cc; - } - return [ - 'hashtags' => [ - 'enabled' => false, - ], - 'memories' => [ - 'enabled' => false, - ], - 'insights' => [ - 'enabled' => false, - ], - 'friends' => [ - 'enabled' => false, - ], - 'server' => [ - 'enabled' => false, - 'mode' => 'allowlist', - 'domains' => [] - ] - ]; - } + public function trendingHashtags(Request $request) + { + abort_if(! $request->user(), 403); - public function serverTimeline(Request $request) - { - abort_if(!$request->user(), 404); - abort_if(!$this->config()['server']['enabled'], 404); - $pid = $request->user()->profile_id; - $domain = $request->input('domain'); - $config = $this->config(); - $domains = explode(',', $config['server']['domains']); - abort_unless(in_array($domain, $domains), 400); + $res = TrendingHashtagService::getTrending(); - $res = Status::whereNotNull('uri') - ->where('uri', 'like', 'https://' . $domain . '%') - ->whereNull(['in_reply_to_id', 'reblog_of_id']) - ->orderByDesc('id') - ->limit(12) - ->pluck('id') - ->map(function($id) { - return StatusService::get($id); - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); - return $res; - } + return $res; + } - public function enabledFeatures(Request $request) - { - abort_if(!$request->user(), 404); - return $this->config(); - } + public function trendingPlaces(Request $request) + { + return []; + } - public function updateFeatures(Request $request) - { - abort_if(!$request->user(), 404); - abort_if(!$request->user()->is_admin, 404); - $pid = $request->user()->profile_id; - $this->validate($request, [ - 'features.friends.enabled' => 'boolean', - 'features.hashtags.enabled' => 'boolean', - 'features.insights.enabled' => 'boolean', - 'features.memories.enabled' => 'boolean', - 'features.server.enabled' => 'boolean', - ]); - $res = $request->input('features'); - if($res['server'] && isset($res['server']['domains']) && !empty($res['server']['domains'])) { - $parts = explode(',', $res['server']['domains']); - $parts = array_filter($parts, function($v) { - $len = strlen($v); - $pos = strpos($v, '.'); - $domain = trim($v); - if($pos == false || $pos == ($len + 1)) { - return false; - } - if(!Instance::whereDomain($domain)->exists()) { - return false; - } - return true; - }); - $parts = array_slice($parts, 0, 10); - $d = implode(',', array_map('trim', $parts)); - $res['server']['domains'] = $d; - } - ConfigCacheService::put('config.discover.features', json_encode($res)); - return $res; - } + public function myMemories(Request $request) + { + abort_if(! $request->user(), 404); + $pid = $request->user()->profile_id; + abort_if(! $this->config()['memories']['enabled'], 404); + $type = $request->input('type') ?? 'posts'; + + switch ($type) { + case 'posts': + $res = Status::whereProfileId($pid) + ->whereDay('created_at', date('d')) + ->whereMonth('created_at', date('m')) + ->whereYear('created_at', '!=', date('Y')) + ->whereNull(['reblog_of_id', 'in_reply_to_id']) + ->limit(20) + ->pluck('id') + ->map(function ($id) { + return StatusService::get($id, false); + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); + break; + + case 'liked': + $res = Like::whereProfileId($pid) + ->whereDay('created_at', date('d')) + ->whereMonth('created_at', date('m')) + ->whereYear('created_at', '!=', date('Y')) + ->orderByDesc('status_id') + ->limit(20) + ->pluck('status_id') + ->map(function ($id) { + $status = StatusService::get($id, false); + $status['favourited'] = true; + + return $status; + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); + break; + } + + return $res; + } + + public function accountInsightsPopularPosts(Request $request) + { + abort_if(! $request->user(), 404); + $pid = $request->user()->profile_id; + abort_if(! $this->config()['insights']['enabled'], 404); + $posts = Cache::remember('pf:discover:metro2:accinsights:popular:'.$pid, 43200, function () use ($pid) { + return Status::whereProfileId($pid) + ->whereNotNull('likes_count') + ->orderByDesc('likes_count') + ->limit(12) + ->pluck('id') + ->map(function ($id) { + return StatusService::get($id, false); + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); + }); + + return $posts; + } + + public function config() + { + $cc = ConfigCacheService::get('config.discover.features'); + if ($cc) { + return is_string($cc) ? json_decode($cc, true) : $cc; + } + + return [ + 'hashtags' => [ + 'enabled' => false, + ], + 'memories' => [ + 'enabled' => false, + ], + 'insights' => [ + 'enabled' => false, + ], + 'friends' => [ + 'enabled' => false, + ], + 'server' => [ + 'enabled' => false, + 'mode' => 'allowlist', + 'domains' => [], + ], + ]; + } + + public function serverTimeline(Request $request) + { + abort_if(! $request->user(), 404); + abort_if(! $this->config()['server']['enabled'], 404); + $pid = $request->user()->profile_id; + $domain = $request->input('domain'); + $config = $this->config(); + $domains = explode(',', $config['server']['domains']); + abort_unless(in_array($domain, $domains), 400); + + $res = Status::whereNotNull('uri') + ->where('uri', 'like', 'https://'.$domain.'%') + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->orderByDesc('id') + ->limit(12) + ->pluck('id') + ->map(function ($id) { + return StatusService::get($id); + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); + + return $res; + } + + public function enabledFeatures(Request $request) + { + abort_if(! $request->user(), 404); + + return $this->config(); + } + + public function updateFeatures(Request $request) + { + abort_if(! $request->user(), 404); + abort_if(! $request->user()->is_admin, 404); + $pid = $request->user()->profile_id; + $this->validate($request, [ + 'features.friends.enabled' => 'boolean', + 'features.hashtags.enabled' => 'boolean', + 'features.insights.enabled' => 'boolean', + 'features.memories.enabled' => 'boolean', + 'features.server.enabled' => 'boolean', + ]); + $res = $request->input('features'); + if ($res['server'] && isset($res['server']['domains']) && ! empty($res['server']['domains'])) { + $parts = explode(',', $res['server']['domains']); + $parts = array_filter($parts, function ($v) { + $len = strlen($v); + $pos = strpos($v, '.'); + $domain = trim($v); + if ($pos == false || $pos == ($len + 1)) { + return false; + } + if (! Instance::whereDomain($domain)->exists()) { + return false; + } + + return true; + }); + $parts = array_slice($parts, 0, 10); + $d = implode(',', array_map('trim', $parts)); + $res['server']['domains'] = $d; + } + ConfigCacheService::put('config.discover.features', json_encode($res)); + + return $res; + } }