commands.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import {osu_fetch} from './api.js';
  2. import bancho from './bancho.js';
  3. import db from './database.js';
  4. import {get_user_rank} from './elo.js';
  5. import Config from './util/config.js';
  6. import {gen_url} from './util/helpers.js';
  7. async function reply(user, lobby, message) {
  8. if (lobby) {
  9. await lobby.send(`${user}: ${message}`);
  10. } else {
  11. await bancho.privmsg(user, message);
  12. }
  13. }
  14. async function rank_command(msg, match, lobby) {
  15. const requested_username = match[1].trim() || msg.from;
  16. const res = db.prepare(`SELECT user_id FROM user WHERE username = ?`).get(requested_username);
  17. let user_id = null;
  18. if (res) {
  19. user_id = res.user_id;
  20. } else {
  21. try {
  22. user_id = await bancho.whois(requested_username);
  23. } catch (err) {
  24. user_id = null;
  25. }
  26. }
  27. const rank = get_user_rank(user_id, lobby?.data?.ruleset || 0);
  28. if (!rank) {
  29. await reply(msg.from, lobby, `${requested_username} hasn't played in a ranked lobby yet.`);
  30. return;
  31. }
  32. const user_page = gen_url(lobby?.data?.ruleset, `/u/${user_id}/`);
  33. await reply(msg.from, lobby, `[${user_page} ${requested_username}] | Rank: ${rank.text} (#${rank.rank_nb}) | Elo: ${rank.fancy_elo} | Games played: ${rank.total_scores}`);
  34. }
  35. async function start_command(msg, match, lobby) {
  36. if (lobby.countdown != -1 || lobby.playing) return;
  37. if (lobby.players.length < 2) {
  38. await lobby.send(`!mp start .${Math.random().toString(36).substring(2, 6)}`);
  39. return;
  40. }
  41. lobby.countdown = setTimeout(async () => {
  42. if (lobby.playing) {
  43. lobby.countdown = -1;
  44. return;
  45. }
  46. lobby.countdown = setTimeout(async () => {
  47. lobby.countdown = -1;
  48. if (!lobby.playing) {
  49. await lobby.send(`!mp start .${Math.random().toString(36).substring(2, 6)}`);
  50. }
  51. }, 10000);
  52. await lobby.send('Starting the match in 10 seconds... Ready up to start sooner.');
  53. }, 20000);
  54. await lobby.send('Starting the match in 30 seconds... Ready up to start sooner.');
  55. }
  56. async function wait_command(msg, match, lobby) {
  57. if (lobby.countdown == -1) return;
  58. clearTimeout(lobby.countdown);
  59. lobby.countdown = -1;
  60. await lobby.send('Match auto-start is cancelled. Type !start to restart it.');
  61. }
  62. async function about_command(msg, match, lobby) {
  63. const faq_page = gen_url(lobby?.data?.ruleset, `/faq/`);
  64. if (lobby) {
  65. await lobby.send(`In this lobby, you get a rank based on how well you score against other players. More info: ${faq_page}`);
  66. } else {
  67. await bancho.privmsg(msg.from, faq_page);
  68. }
  69. }
  70. async function discord_command(msg, match, lobby) {
  71. await reply(msg.from, lobby, `[${Config.discord_invite_link} Come hang out in voice chat!] (or just text, no pressure)`);
  72. }
  73. async function abort_command(msg, match, lobby) {
  74. if (!lobby.playing) {
  75. await lobby.send(`${msg.from}: The match has not started, cannot abort.`);
  76. return;
  77. }
  78. if (!lobby.voteaborts.includes(msg.from)) {
  79. lobby.voteaborts.push(msg.from);
  80. const nb_voted_to_abort = lobby.voteaborts.length;
  81. const nb_required_to_abort = Math.ceil(lobby.players.length / 4);
  82. if (lobby.voteaborts.length >= nb_required_to_abort) {
  83. await lobby.send(`!mp abort ${Math.random().toString(36).substring(2, 6)}`);
  84. lobby.voteaborts = [];
  85. await lobby.select_next_map();
  86. } else {
  87. await lobby.send(`${msg.from} voted to abort the match. ${nb_voted_to_abort}/${nb_required_to_abort} votes needed.`);
  88. }
  89. }
  90. }
  91. async function close_command(msg, match, lobby) {
  92. lobby.dont_reopen = true;
  93. await lobby.send(`!mp close ${Math.random().toString(36).substring(2, 6)}`);
  94. }
  95. async function ban_command(msg, match, lobby) {
  96. const bad_player = match[1].trim();
  97. if (bad_player == '') {
  98. await lobby.send(msg.from + ': You need to specify which player to ban.');
  99. return;
  100. }
  101. if (!lobby.votekicks[bad_player]) {
  102. lobby.votekicks[bad_player] = [];
  103. }
  104. if (!lobby.votekicks[bad_player].includes(msg.from)) {
  105. lobby.votekicks[bad_player].push(msg.from);
  106. const nb_voted_to_kick = lobby.votekicks[bad_player].length;
  107. let nb_required_to_kick = Math.ceil(lobby.players.length / 2);
  108. if (nb_required_to_kick == 1) nb_required_to_kick = 2; // don't allow a player to hog the lobby
  109. if (nb_voted_to_kick >= nb_required_to_kick) {
  110. await lobby.send('!mp ban ' + bad_player);
  111. } else {
  112. await lobby.send(`${msg.from} voted to ban ${bad_player}. ${nb_voted_to_kick}/${nb_required_to_kick} votes needed.`);
  113. }
  114. }
  115. }
  116. async function skip_command(msg, match, lobby) {
  117. if (lobby.players.length < 5) {
  118. await lobby.select_next_map();
  119. return;
  120. }
  121. // Skip map if DMCA'd
  122. // When bot just joined the lobby, beatmap_id is null.
  123. if (lobby.beatmap_id && !lobby.map_data) {
  124. try {
  125. console.info(`[API] Fetching map data for map ID ${lobby.beatmap_id}`);
  126. lobby.map_data = await osu_fetch(`https://osu.ppy.sh/api/v2/beatmaps/lookup?id=${lobby.beatmap_id}`);
  127. if (lobby.map_data.beatmapset.availability.download_disabled) {
  128. clearTimeout(lobby.countdown);
  129. lobby.countdown = -1;
  130. db.prepare(`UPDATE map SET dmca = 1 WHERE map_id = ?`).run(lobby.beatmap_id);
  131. await lobby.select_next_map();
  132. await lobby.send(`Skipped previous map because download was unavailable [${lobby.map_data.beatmapset.availability.more_information} (more info)].`);
  133. return;
  134. }
  135. } catch (err) {
  136. console.error(`Failed to fetch map data for beatmap #${lobby.beatmap_id}: ${err}`);
  137. }
  138. }
  139. // Skip map if player is lobby creator
  140. let user_is_creator = false;
  141. for (const player of lobby.players) {
  142. if (player.irc_username == msg.from) {
  143. user_is_creator = player.user_id == lobby.data.creator_id;
  144. break;
  145. }
  146. }
  147. if (user_is_creator) {
  148. await lobby.select_next_map();
  149. return;
  150. }
  151. // Skip map if player has been in the lobby long enough
  152. for (const player of lobby.players) {
  153. if (player.irc_username == msg.from) {
  154. // Make sure the field is initialized
  155. if (!player.matches_finished) {
  156. player.matches_finished = 0;
  157. }
  158. if (player.matches_finished >= 5) {
  159. player.matches_finished = 0;
  160. await lobby.select_next_map();
  161. } else {
  162. await reply(msg.from, lobby, `You need to play ${5 - player.matches_finished} more matches in this lobby before you can skip.`);
  163. }
  164. return;
  165. }
  166. }
  167. await reply(msg.from, lobby, `You need to play 5 more matches in this lobby before you can skip.`);
  168. }
  169. const commands = [
  170. {
  171. regex: /^!about$/i,
  172. handler: about_command,
  173. creator_only: false,
  174. modes: ['pm', 'lobby'],
  175. },
  176. {
  177. regex: /^!info/i,
  178. handler: about_command,
  179. creator_only: false,
  180. modes: ['pm', 'lobby'],
  181. },
  182. {
  183. regex: /^!help$/i,
  184. handler: about_command,
  185. creator_only: false,
  186. modes: ['pm', 'lobby'],
  187. },
  188. {
  189. regex: /^!discord$/i,
  190. handler: discord_command,
  191. creator_only: false,
  192. modes: ['pm', 'lobby'],
  193. },
  194. {
  195. regex: /^!rank(.*)/i,
  196. handler: rank_command,
  197. creator_only: false,
  198. modes: ['pm', 'lobby'],
  199. },
  200. {
  201. regex: /^!abort$/i,
  202. handler: abort_command,
  203. creator_only: false,
  204. modes: ['lobby'],
  205. },
  206. {
  207. regex: /^!close$/i,
  208. handler: close_command,
  209. creator_only: true,
  210. modes: ['lobby'],
  211. },
  212. {
  213. regex: /^!start$/i,
  214. handler: start_command,
  215. creator_only: false,
  216. modes: ['lobby'],
  217. },
  218. {
  219. regex: /^!wait$/i,
  220. handler: wait_command,
  221. creator_only: false,
  222. modes: ['lobby'],
  223. },
  224. {
  225. regex: /^!stop$/i,
  226. handler: wait_command,
  227. creator_only: false,
  228. modes: ['lobby'],
  229. },
  230. {
  231. regex: /^!ban(.*)/i,
  232. handler: ban_command,
  233. creator_only: false,
  234. modes: ['lobby'],
  235. },
  236. {
  237. regex: /^!kick(.*)/i,
  238. handler: ban_command,
  239. creator_only: false,
  240. modes: ['lobby'],
  241. },
  242. {
  243. regex: /^!skip$/i,
  244. handler: skip_command,
  245. creator_only: false,
  246. modes: ['lobby'],
  247. },
  248. ];
  249. export default commands;