the browser-facing portion of osu!
at master 10 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\Libraries\CommentBundle; 9use App\Models\NewsPost; 10 11/** 12 * @group News 13 */ 14class NewsController extends Controller 15{ 16 /** 17 * Get News Listing 18 * 19 * Returns a list of news posts and related metadata. 20 * 21 * --- 22 * 23 * ### Response Format 24 * 25 * Field | Type | Notes 26 * ------------------------- | ----------------------------- | ----- 27 * cursor_string | [CursorString](#cursorstring) | | 28 * news_posts | [NewsPost](#newspost)[] | Includes `preview`. 29 * news_sidebar.current_year | integer | Year of the first post's publish time, or current year if no posts returned. 30 * news_sidebar.news_posts | [NewsPost](#newspost)[] | All posts published during `current_year`. 31 * news_sidebar.years | integer[] | All years during which posts have been published. 32 * search.limit | integer | Clamped limit input. 33 * search.sort | string | Always `published_desc`. 34 * 35 * <aside class="notice"> 36 * <a href="#newspost">NewsPost</a> collections queried by year will also include posts published in November and December of the previous year if the current date is the same year and before April. 37 * </aside> 38 * 39 * @usesCursor 40 * @queryParam limit integer Maximum number of posts (12 default, 1 minimum, 21 maximum). No-example 41 * @queryParam year integer Year to return posts from. No-example 42 * @response { 43 * "news_posts": [ 44 * { 45 * "id": 964, 46 * "author": "RockRoller", 47 * "edit_url": "https://github.com/ppy/osu-wiki/tree/master/news/2021-05-27-skinning-contest-results.md", 48 * "first_image": "https://i.ppy.sh/d431ff921955d5c8792dc9bae40ac082d4e53131/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f7368617265642f6e6577732f323032312d30352d32372d736b696e6e696e672d636f6e746573742d726573756c74732f736b696e6e696e675f636f6e746573745f62616e6e65722e6a7067", 49 * "published_at": "2021-05-27T12:00:00+00:00", 50 * "updated_at": "2021-05-28T17:11:35+00:00", 51 * "slug": "2021-05-27-skinning-contest-results", 52 * "title": "Skinning Contest: Results Out", 53 * "preview": "The ship full of skins is now back with your votes. Check out the results for our first-ever official skinning contest right here!" 54 * }, 55 * // ... 56 * ], 57 * "news_sidebar": { 58 * "current_year": 2021, 59 * "news_posts": [ 60 * { 61 * "id": 964, 62 * "author": "RockRoller", 63 * "edit_url": "https://github.com/ppy/osu-wiki/tree/master/news/2021-05-27-skinning-contest-results.md", 64 * "first_image": "https://i.ppy.sh/d431ff921955d5c8792dc9bae40ac082d4e53131/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f7368617265642f6e6577732f323032312d30352d32372d736b696e6e696e672d636f6e746573742d726573756c74732f736b696e6e696e675f636f6e746573745f62616e6e65722e6a7067", 65 * "published_at": "2021-05-27T12:00:00+00:00", 66 * "updated_at": "2021-05-28T17:11:35+00:00", 67 * "slug": "2021-05-27-skinning-contest-results", 68 * "title": "Skinning Contest: Results Out" 69 * }, 70 * // ... 71 * ], 72 * "years": [2021, 2020, 2019, 2018, 2017, 2016, 2015, 2014, 2013] 73 * }, 74 * "search": { 75 * "limit": 12, 76 * "sort": "published_desc" 77 * }, 78 * "cursor_string": "WyJodHRwczpcL1wvd3d3LnlvdXR1YmUuY29tXC93YXRjaD92PWRRdzR3OVdnWGNRIl0" 79 * } 80 */ 81 public function index() 82 { 83 $params = request()->all(); 84 $format = $params['format'] ?? null; 85 $isFeed = $format === 'atom' || $format === 'rss'; 86 $limit = $isFeed ? 20 : 12; 87 88 $search = NewsPost::search(array_merge(compact('limit'), $params)); 89 90 [$posts, $hasMore] = $search['query']->getWithHasMore(); 91 92 if ($isFeed) { 93 return ext_view("news.index-{$format}", compact('posts'), $format); 94 } 95 96 $postsJson = [ 97 'news_posts' => json_collection($posts, 'NewsPost', ['preview']), 98 'news_sidebar' => $this->sidebarMeta($posts[0] ?? null), 99 'search' => $search['params'], 100 ...cursor_for_response($search['cursorHelper']->next($posts, $hasMore)), 101 ]; 102 103 if (is_json_request()) { 104 return $postsJson; 105 } else { 106 // force current year search parameter for html 107 $postsJson['search']['year'] = $postsJson['news_sidebar']['current_year']; 108 109 return ext_view('news.index', [ 110 'atom' => [ 111 'url' => route('news.index', ['format' => 'atom']), 112 'title' => 'osu!news Feed', 113 ], 114 'postsJson' => $postsJson, 115 ]); 116 } 117 } 118 119 public function redirect($tumblrId) 120 { 121 $post = NewsPost::where('tumblr_id', $tumblrId)->firstOrFail(); 122 123 return ujs_redirect(route('news.show', $post->slug)); 124 } 125 126 /** 127 * Get News Post 128 * 129 * Returns details of the specified news post. 130 * 131 * --- 132 * 133 * ### Response Format 134 * 135 * Returns a [NewsPost](#newspost) with `content` and `navigation` included. 136 * 137 * @urlParam news string required News post slug or ID. Example: 2021-04-27-results-a-labour-of-love 138 * @queryParam key string Unset to query by slug, or `id` to query by ID. No-example 139 * @response { 140 * "id": 943, 141 * "author": "pishifat", 142 * "edit_url": "https://github.com/ppy/osu-wiki/tree/master/news/2021-04-27-results-a-labour-of-love.md", 143 * "first_image": "https://i.ppy.sh/65c9c2eb2f8d9bc6008b95aba7d0ef45e1414c1e/68747470733a2f2f6f73752e7070792e73682f77696b692f696d616765732f7368617265642f6e6577732f323032302d31312d33302d612d6c61626f75722d6f662d6c6f76652f616c6f6c5f636f7665722e6a7067", 144 * "published_at": "2021-04-27T20:00:00+00:00", 145 * "updated_at": "2021-04-27T20:25:57+00:00", 146 * "slug": "2021-04-27-results-a-labour-of-love", 147 * "title": "Results - A Labour of Love", 148 * "content": "<div class='osu-md osu-md--news'>...</div>", 149 * "navigation": { 150 * "newer": { 151 * "id": 944, 152 * "author": "pishifat", 153 * "edit_url": "https://github.com/ppy/osu-wiki/tree/master/news/2021-04-28-new-featured-artist-emilles-moonlight-serenade.md", 154 * "first_image": "https://i.ppy.sh/7e22cc5f4755c21574d999d8ce3a2f40a3268e84/68747470733a2f2f6173736574732e7070792e73682f617274697374732f3136302f6865616465722e6a7067", 155 * "published_at": "2021-04-28T08:00:00+00:00", 156 * "updated_at": "2021-04-28T09:51:28+00:00", 157 * "slug": "2021-04-28-new-featured-artist-emilles-moonlight-serenade", 158 * "title": "New Featured Artist: Emille's Moonlight Serenade" 159 * }, 160 * "older": { 161 * "id": 942, 162 * "author": "pishifat", 163 * "edit_url": "https://github.com/ppy/osu-wiki/tree/master/news/2021-04-24-new-featured-artist-grynpyret.md", 164 * "first_image": "https://i.ppy.sh/acdce813b71371b95e8240f9249c916285fdc5a0/68747470733a2f2f6173736574732e7070792e73682f617274697374732f3135392f6865616465722e6a7067", 165 * "published_at": "2021-04-24T08:00:00+00:00", 166 * "updated_at": "2021-04-24T10:23:59+00:00", 167 * "slug": "2021-04-24-new-featured-artist-grynpyret", 168 * "title": "New Featured Artist: Grynpyret" 169 * } 170 * } 171 * } 172 */ 173 public function show($slug) 174 { 175 if (request('key') === 'id') { 176 $post = NewsPost::findOrFail($slug); 177 178 if (!is_api_request()) { 179 return ujs_redirect(route('news.show', $post->slug)); 180 } 181 } else { 182 $post = NewsPost::lookup($slug); 183 } 184 185 $post->sync(); 186 187 if (!$post->isVisible()) { 188 abort(404); 189 } 190 191 $postJson = json_item($post, 'NewsPost', ['content', 'navigation']); 192 193 if (is_json_request()) { 194 return $postJson; 195 } 196 197 set_opengraph($post); 198 199 return ext_view('news.show', [ 200 'commentBundle' => CommentBundle::forEmbed($post), 201 'post' => $post, 202 'postJson' => $postJson, 203 'sidebarMeta' => $this->sidebarMeta($post), 204 ]); 205 } 206 207 public function store() 208 { 209 priv_check('NewsIndexUpdate')->ensureCan(); 210 211 NewsPost::syncAll(); 212 213 return ['message' => osu_trans('news.store.ok')]; 214 } 215 216 public function update($id) 217 { 218 priv_check('NewsPostUpdate')->ensureCan(); 219 220 NewsPost::findOrFail($id)->sync(true); 221 222 return ['message' => osu_trans('news.update.ok')]; 223 } 224 225 private function sidebarMeta($post) 226 { 227 if ($post !== null && $post->published_at !== null) { 228 $currentYear = $post->published_at->year; 229 } 230 231 $currentYear = $currentYear ?? date('Y'); 232 $latestPost = NewsPost::select('updated_at')->default()->first(); 233 $lastUpdate = $latestPost === null ? 0 : $latestPost->updated_at->timestamp; 234 235 return cache_remember_with_fallback( 236 "news_sidebar_meta_{$currentYear}_{$lastUpdate}", 237 3600, 238 function () use ($currentYear) { 239 $years = NewsPost::selectRaw('DISTINCT YEAR(published_at) year') 240 ->whereNotNull('published_at') 241 ->orderBy('year', 'DESC') 242 ->pluck('year') 243 ->toArray(); 244 245 $posts = NewsPost::default()->year($currentYear)->get(); 246 247 return [ 248 'current_year' => $currentYear, 249 'news_posts' => json_collection($posts, 'NewsPost'), 250 'years' => $years, 251 ]; 252 } 253 ); 254 } 255}