website_ssr.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import fs from 'fs';
  2. import {Eta} from 'eta';
  3. import twemoji from '@twemoji/api';
  4. import Config from './util/config.js';
  5. import countries from './countries.js';
  6. import db from './database.js';
  7. import {get_user} from './user.js';
  8. import {get_user_rank} from './elo.js';
  9. import {get_user_matches, get_leaderboard_page} from './website_api.js';
  10. const eta = new Eta({
  11. autoTrim: ['nl', 'slurp'],
  12. cache: true,
  13. debug: !Config.IS_PRODUCTION,
  14. views: './views',
  15. });
  16. const base = fs.readFileSync('views/main.eta', 'utf-8');
  17. const user_not_found_page = base.replace('<%~ it.body %>', 'User not found.'); // TODO better looking page
  18. const faq_page = eta.render('faq.eta');
  19. const js_page = eta.render('jspage.eta');
  20. function mini_cache(res) {
  21. if (!Config.IS_PRODUCTION) return;
  22. // Cache that only lasts 10 seconds, to reduce load while still keeping content fresh
  23. res.set('Cache-Control', 'public, max-age=10');
  24. }
  25. function get_mode(req) {
  26. const ruleset = req.subdomains[0] || 'osu';
  27. if (ruleset == 'osu') {
  28. return 0;
  29. } else if (ruleset == 'taiko') {
  30. return 1;
  31. } else if (ruleset == 'catch') {
  32. return 2;
  33. } else if (ruleset == 'mania') {
  34. return 3;
  35. } else {
  36. throw RULESET_NOT_FOUND;
  37. }
  38. }
  39. function country_code_to_flag_html(country_code) {
  40. // Unknown country
  41. if (country_code?.length != 2) {
  42. return `<img class="emoji" draggable="false" alt="?" src="/unknown_flag.png" />`;
  43. }
  44. const codepoints = country_code.split('').map((char) => 127397 + char.charCodeAt(0));
  45. const emoji = String.fromCodePoint(...codepoints);
  46. return twemoji.parse(emoji);
  47. }
  48. async function render_user_profile(req, res, page_num) {
  49. mini_cache(res);
  50. const mode = get_mode(req);
  51. const user = await get_user(req.params.userId);
  52. if (!user) {
  53. return res.status(404).end(user_not_found_page);
  54. }
  55. const rank = get_user_rank(user.user_id, mode);
  56. if (!rank) {
  57. return res.status(404).end(user_not_found_page);
  58. }
  59. mini_cache(res);
  60. // Dirty hack to make Discord embeds work
  61. if (req.get('User-Agent').indexOf('Discordbot') != -1) {
  62. return res.end(`<html>
  63. <head>
  64. <meta content="${user.username} - o!RL" property="og:title" />
  65. <meta content="#${rank.rank_nb} - ${rank.text}" property="og:description" />
  66. <meta content="https://${req.hostname}/u/${user.user_id}" property="og:url" />
  67. <meta content="https://s.ppy.sh/a/${user.user_id}" property="og:image" />
  68. </head>
  69. <body>hi :)</body>
  70. </html>`);
  71. }
  72. let elo = rank.elo;
  73. const elo_evolution = [elo];
  74. const last_90 = db.prepare(
  75. `SELECT elo_diff FROM score
  76. WHERE user_id = ? AND mode = ?
  77. ORDER BY created_at DESC LIMIT 29`,
  78. ).all(user.user_id, mode);
  79. for (const row of last_90) {
  80. elo -= row.elo_diff;
  81. elo_evolution.unshift(elo);
  82. }
  83. const osu_rulesets = ['osu', 'taiko', 'fruits', 'mania'];
  84. const matches = await get_user_matches(user.user_id, mode, page_num);
  85. return res.end(eta.render('user.eta', {
  86. user, rank, matches, elo_evolution,
  87. flag: country_code_to_flag_html(user.country_code),
  88. country_name: countries[user.country_code] || 'Unknown country',
  89. osu_ruleset: osu_rulesets[mode],
  90. }));
  91. }
  92. async function render_leaderboard(req, res, page_num) {
  93. mini_cache(res);
  94. const data = await get_leaderboard_page(get_mode(req), page_num);
  95. for (const player of data.players) {
  96. player.flag = country_code_to_flag_html(player.country_code);
  97. }
  98. return res.end(eta.render('leaderboard.eta', data));
  99. }
  100. async function register_routes(app) {
  101. const get_page_num = (req) => {
  102. let page_num = 1;
  103. if (req.params.pageStr.indexOf('page-') == 0) {
  104. page_num = parseInt(req.params.pageStr.split('-')[1], 10);
  105. if (page_num <= 0 || isNaN(page_num)) {
  106. page_num = 1;
  107. }
  108. }
  109. return page_num;
  110. };
  111. app.get('/faq/', (req, res) => {
  112. mini_cache(res); res.end(faq_page);
  113. });
  114. app.get('/lobbies/', (req, res) => {
  115. mini_cache(res); res.end(js_page);
  116. });
  117. // Login-gated lobby creation
  118. app.get('/create-lobby/', (req, res) => {
  119. if (req.signedCookies.user) {
  120. return res.end(js_page);
  121. } else {
  122. res.cookie('login_to', `${req.protocol}://${req.hostname}/create-lobby/`, {
  123. domain: Config.domain_name,
  124. maxAge: 30000,
  125. httpOnly: false,
  126. secure: Config.IS_PRODUCTION,
  127. signed: false,
  128. sameSite: 'Strict',
  129. });
  130. return res.redirect('/osu_login');
  131. }
  132. });
  133. // Login-gated lobby reopening
  134. app.get('/reopen-lobby/*', (req, res) => {
  135. if (req.signedCookies.user) {
  136. return res.end(js_page);
  137. } else {
  138. res.cookie('login_to', `${req.protocol}://${req.hostname}${req.originalUrl}`, {
  139. domain: Config.domain_name,
  140. maxAge: 30000,
  141. httpOnly: false,
  142. secure: Config.IS_PRODUCTION,
  143. signed: false,
  144. sameSite: 'Strict',
  145. });
  146. return res.redirect('/osu_login');
  147. }
  148. });
  149. // User pages
  150. app.get('/u/:userId/:pageStr/', (req, res) => {
  151. const page_num = get_page_num(req);
  152. return render_user_profile(req, res, page_num);
  153. });
  154. app.get('/u/:userId', (req, res) => {
  155. return render_user_profile(req, res, 1);
  156. });
  157. // Leaderboard pages
  158. app.get('/leaderboard/:pageStr/', (req, res) => {
  159. const page_num = get_page_num(req);
  160. return render_leaderboard(req, res, page_num);
  161. });
  162. app.get('/leaderboard/', (req, res) => {
  163. return render_leaderboard(req, res, 1);
  164. });
  165. }
  166. export {
  167. register_routes,
  168. };