the browser-facing portion of osu!
at master 15 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6namespace App\Http\Controllers; 7 8use App; 9use App\Libraries\CurrentStats; 10use App\Libraries\MenuContent; 11use App\Libraries\Search\AllSearch; 12use App\Libraries\Search\QuickSearch; 13use App\Models\BeatmapDownload; 14use App\Models\Beatmapset; 15use App\Models\Forum\Post; 16use App\Models\NewsPost; 17use App\Models\UserDonation; 18use App\Transformers\MenuImageTransformer; 19use App\Transformers\UserCompactTransformer; 20use Auth; 21use Jenssegers\Agent\Agent; 22use Request; 23 24/** 25 * @group Home 26 */ 27class HomeController extends Controller 28{ 29 public function __construct() 30 { 31 $this->middleware('auth', [ 32 'only' => [ 33 'downloadQuotaCheck', 34 'quickSearch', 35 ], 36 ]); 37 38 $this->middleware('require-scopes:public', ['only' => 'search']); 39 40 parent::__construct(); 41 } 42 43 public function bbcodePreview() 44 { 45 $post = new Post(['post_text' => Request::input('text')]); 46 47 return $post->bodyHTML(); 48 } 49 50 /** 51 * @group Undocumented 52 */ 53 public function downloadQuotaCheck() 54 { 55 return [ 56 'quota_used' => BeatmapDownload::where('user_id', Auth::user()->user_id)->count(), 57 ]; 58 } 59 60 public function getDownload() 61 { 62 $lazerPlatformNames = [ 63 'android' => osu_trans('home.download.os_version_or_later', ['os_version' => 'Android 5']), 64 'ios' => osu_trans('home.download.os_version_or_later', ['os_version' => 'iOS 13.4']), 65 'linux_x64' => 'Linux (x64)', 66 'macos_as' => osu_trans('home.download.os_version_or_later', ['os_version' => 'macOS 10.15']).' (Apple Silicon)', 67 'windows_x64' => osu_trans('home.download.os_version_or_later', ['os_version' => 'Windows 8.1']).' (x64)', 68 ]; 69 70 $agent = new Agent(Request::server()); 71 72 $platform = match (true) { 73 // Try matching most likely platform first 74 $agent->is('Windows') => 'windows_x64', 75 // iPadOS detection apparently doesn't work on newer version 76 // and detected as macOS instead. 77 ($agent->isiOS() || $agent->isiPadOS()) => $platform = 'ios', 78 // FIXME: Figure out a way to differentiate Intel and Apple Silicon. 79 $agent->is('OS X') => 'macos_as', 80 $agent->isAndroidOS() => 'android', 81 $agent->is('Linux') => 'linux_x64', 82 default => 'windows_x64', 83 }; 84 85 return ext_view('home.download', [ 86 'lazerUrl' => osu_url("lazer_dl.{$platform}"), 87 'lazerPlatformName' => $lazerPlatformNames[$platform], 88 ]); 89 } 90 91 public function index() 92 { 93 $host = Request::getHttpHost(); 94 $subdomain = substr($host, 0, strpos($host, '.')); 95 96 if ($subdomain === 'store') { 97 return ujs_redirect(route('store.products.index')); 98 } 99 100 $newsLimit = Auth::check() ? NewsPost::DASHBOARD_LIMIT + 1 : NewsPost::LANDING_LIMIT; 101 $news = NewsPost::default()->limit($newsLimit)->get(); 102 103 if (Auth::check()) { 104 $menuImages = json_collection(MenuContent::activeImages(), new MenuImageTransformer()); 105 $newBeatmapsets = Beatmapset::latestRanked(); 106 $popularBeatmapsets = Beatmapset::popular()->get(); 107 108 return ext_view('home.user', compact( 109 'menuImages', 110 'newBeatmapsets', 111 'news', 112 'popularBeatmapsets' 113 )); 114 } else { 115 $news = json_collection($news, 'NewsPost'); 116 117 return ext_view('home.landing', ['stats' => new CurrentStats(), 'news' => $news]); 118 } 119 } 120 121 public function messageUser($user) 122 { 123 return ujs_redirect(route('chat.index', ['sendto' => $user])); 124 } 125 126 public function opensearch() 127 { 128 return ext_view('home.opensearch', null, 'opensearch')->header('Cache-Control', 'max-age=86400'); 129 } 130 131 public function quickSearch() 132 { 133 $quickSearch = new QuickSearch(Request::all(), ['user' => auth()->user()]); 134 $searches = $quickSearch->searches(); 135 136 $result = []; 137 138 if ($quickSearch->hasQuery()) { 139 foreach ($searches as $mode => $search) { 140 if ($search === null) { 141 continue; 142 } 143 $result[$mode]['total'] = $search->count(); 144 } 145 146 $result['user']['users'] = json_collection( 147 $searches['user']->data(), 148 new UserCompactTransformer(), 149 [...UserCompactTransformer::CARD_INCLUDES, 'support_level'], 150 ); 151 $result['beatmapset']['beatmapsets'] = json_collection($searches['beatmapset']->data(), 'Beatmapset', ['beatmaps']); 152 } 153 154 return $result; 155 } 156 157 /** 158 * Search 159 * 160 * Searches users and wiki pages. 161 * 162 * --- 163 * 164 * ### Response Format 165 * 166 * Field | Type | Description 167 * --------- | -------------------------- | ----------- 168 * user | SearchResult&lt;User>? | For `all` or `user` mode. Only first 100 results are accessible 169 * wiki_page | SearchResult&lt;WikiPage>? | For `all` or `wiki_page` mode 170 * 171 * #### SearchResult&lt;T> 172 * 173 * Field | Type | Description 174 * ----- | ------- | ----------- 175 * data | T[] | | 176 * total | integer | | 177 * 178 * @queryParam mode string Either `all`, `user`, or `wiki_page`. Default is `all`. Example: all 179 * @queryParam query Search keyword. Example: hello 180 * @queryParam page Search result page. Ignored for mode `all`. Example: 1 181 */ 182 public function search() 183 { 184 $currentUser = Auth::user(); 185 $allSearch = new AllSearch(Request::all(), ['user' => $currentUser]); 186 187 if ($allSearch->getMode() === 'beatmapset') { 188 return ujs_redirect(route('beatmapsets.index', ['q' => $allSearch->getRawQuery()])); 189 } 190 191 $isSearchPage = true; 192 193 if (is_api_request()) { 194 return response()->json($allSearch->toJson()); 195 } 196 197 $fields = $currentUser?->isModerator() ?? false ? [] : ['includeDeleted' => null]; 198 199 return ext_view('home.search', compact('allSearch', 'fields', 'isSearchPage')); 200 } 201 202 public function setLocale() 203 { 204 $newLocale = get_valid_locale(Request::input('locale')) ?? $GLOBALS['cfg']['app']['fallback_locale']; 205 App::setLocale($newLocale); 206 207 if (Auth::check()) { 208 Auth::user()->update([ 209 'user_lang' => $newLocale, 210 ]); 211 } 212 213 return ext_view('layout.ujs_full_reload', [], 'js') 214 ->withCookie(cookie()->forever('locale', $newLocale)); 215 } 216 217 public function supportTheGame() 218 { 219 $user = auth()->user(); 220 221 if ($user !== null) { 222 // current status 223 $expiration = $user->osu_subscriptionexpiry?->addDays(1); 224 $current = $expiration?->isFuture() ?? false; 225 226 // purchased 227 $tagPurchases = $user->supporterTagPurchases; 228 $dollars = $tagPurchases->sum('amount'); 229 $cancelledTags = $tagPurchases->where('cancel', true)->count() * 2; // 1 for purchase transaction and 1 for cancel transaction 230 $tags = $tagPurchases->count() - $cancelledTags; 231 232 // gifted 233 $gifted = $tagPurchases->where('target_user_id', '<>', $user->user_id); 234 $giftedDollars = $gifted->sum('amount'); 235 $canceledGifts = $gifted->where('cancel', true)->count() * 2; // 1 for purchase transaction and 1 for cancel transaction 236 $giftedTags = $gifted->count() - $canceledGifts; 237 238 $supporterStatus = [ 239 // current status 240 'current' => $current, 241 'expiration' => $expiration, 242 // purchased 243 'dollars' => currency($dollars, 2, false), 244 'tags' => i18n_number_format($tags), 245 // gifted 246 'giftedDollars' => currency($giftedDollars, 2, false), 247 'giftedTags' => i18n_number_format($giftedTags), 248 ]; 249 250 if ($current) { 251 $lastTagPurchaseDate = UserDonation::where('target_user_id', $user->user_id) 252 ->orderBy('timestamp', 'desc') 253 ->pluck('timestamp') 254 ->first(); 255 256 $lastTagPurchaseDate ??= $expiration->copy()->subMonths(1); 257 258 $total = max(1, $expiration->diffInDays($lastTagPurchaseDate)); 259 $used = $lastTagPurchaseDate->diffInDays(); 260 261 $supporterStatus['remainingPercent'] = 100 - round($used / $total * 100, 2); 262 } 263 } 264 265 $pageLayout = [ 266 // why support 267 'support-reasons' => [ 268 'type' => 'group', 269 'section' => 'why-support', 270 'items' => [ 271 'team' => [ 272 'icons' => ['fas fa-users'], 273 ], 274 'infra' => [ 275 'icons' => ['fas fa-server'], 276 ], 277 'featured-artists' => [ 278 'icons' => ['fas fa-user-astronaut'], 279 'link' => route('artists.index'), 280 ], 281 'ads' => [ 282 'icons' => ['fas fa-ad', 'fas fa-slash'], 283 ], 284 'tournaments' => [ 285 'icons' => ['fas fa-trophy'], 286 'link' => route('tournaments.index'), 287 ], 288 'bounty-program' => [ 289 'icons' => ['fas fa-child'], 290 'link' => osu_url('bounty-form'), 291 ], 292 ], 293 ], 294 295 // supporter perks 296 297 // There are 5 perk rendering types: image, image-flipped, hero, group and image-group. 298 // image, image-flipped, hero each show an individual perk (with image) while group and image-group show groups of perks (the latter with images) 299 'perks' => [ 300 [ 301 'type' => 'image', 302 'name' => 'osu_direct', 303 'icons' => ['fas fa-search'], 304 ], 305 [ 306 'type' => 'image_group', 307 'items' => [ 308 'friend_ranking' => [ 309 'icons' => ['fas fa-list-alt'], 310 ], 311 'country_ranking' => [ 312 'icons' => ['fas fa-globe-asia'], 313 ], 314 'mod_filtering' => [ 315 'icons' => ['fas fa-tasks'], 316 ], 317 ], 318 ], 319 [ 320 'type' => 'image', 321 'variant' => 'flipped', 322 'name' => 'beatmap_filters', 323 'icons' => ['fas fa-filter'], 324 ], 325 [ 326 'type' => 'group', 327 'items' => [ 328 'auto_downloads' => [ 329 'icons' => ['fas fa-download'], 330 ], 331 'more_beatmaps' => [ 332 'icons' => ['fas fa-file-upload'], 333 'translation_options' => [ 334 'base' => $GLOBALS['cfg']['osu']['beatmapset']['upload_allowed'], 335 'bonus' => $GLOBALS['cfg']['osu']['beatmapset']['upload_bonus_per_ranked'], 336 'bonus_max' => $GLOBALS['cfg']['osu']['beatmapset']['upload_bonus_per_ranked_max'], 337 'supporter_base' => $GLOBALS['cfg']['osu']['beatmapset']['upload_allowed_supporter'], 338 'supporter_bonus' => $GLOBALS['cfg']['osu']['beatmapset']['upload_bonus_per_ranked_supporter'], 339 'supporter_bonus_max' => $GLOBALS['cfg']['osu']['beatmapset']['upload_bonus_per_ranked_max_supporter'], 340 ], 341 ], 342 'early_access' => [ 343 'icons' => ['fas fa-flask'], 344 ], 345 ], 346 ], 347 [ 348 'type' => 'hero', 349 'name' => 'customisation', 350 'icons' => ['fas fa-image'], 351 ], 352 [ 353 'type' => 'group', 354 'items' => [ 355 'more_favourites' => [ 356 'icons' => ['fas fa-star'], 357 'translation_options' => [ 358 'normally' => $GLOBALS['cfg']['osu']['beatmapset']['favourite_limit'], 359 'supporter' => $GLOBALS['cfg']['osu']['beatmapset']['favourite_limit_supporter'], 360 ], 361 ], 362 'more_friends' => [ 363 'icons' => ['fas fa-user-friends'], 364 'translation_options' => [ 365 'normally' => $GLOBALS['cfg']['osu']['user']['max_friends'], 366 'supporter' => $GLOBALS['cfg']['osu']['user']['max_friends_supporter'], 367 ], 368 ], 369 'friend_filtering' => [ 370 'icons' => ['fas fa-medal'], 371 ], 372 ], 373 ], 374 [ 375 'type' => 'image_group', 376 'items' => [ 377 'yellow_fellow' => [ 378 'icons' => ['fas fa-fire'], 379 ], 380 'speedy_downloads' => [ 381 'icons' => ['fas fa-tachometer-alt'], 382 ], 383 'change_username' => [ 384 'icons' => ['fas fa-magic'], 385 ], 386 'skinnables' => [ 387 'icons' => ['fas fa-paint-brush'], 388 ], 389 ], 390 ], 391 ], 392 ]; 393 394 return ext_view('home.support-the-game', [ 395 'supporterStatus' => $supporterStatus ?? [], 396 'data' => $pageLayout, 397 ]); 398 } 399 400 public function testflight() 401 { 402 return ext_view('home.testflight'); 403 } 404}