lobby.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import EventEmitter from 'events';
  2. import bancho from './bancho.js';
  3. import commands from './commands.js';
  4. import db from './database.js';
  5. import Config from './util/config.js';
  6. import {capture_sentry_exception} from './util/helpers.js';
  7. import {get_or_create_user} from './user.js';
  8. class BanchoLobby extends EventEmitter {
  9. constructor(channel) {
  10. super();
  11. this.id = parseInt(channel.substring(4), 10);
  12. this.channel = channel;
  13. this.invite_id = null;
  14. // A player is a full_player from the database (safe to assume they have a rank, etc)
  15. // It has an additional irc_username field, that can differ from their actual username.
  16. this.player_cache = [];
  17. this.players = [];
  18. this.scores = [];
  19. this.voteaborts = [];
  20. this.joined = false;
  21. this.playing = false;
  22. const match = db.prepare(`SELECT * FROM match WHERE match_id = ?`).get(this.id);
  23. if (!match) {
  24. db.prepare(`INSERT INTO match (match_id, start_time) VALUES (?, ?)`).run(this.id, Date.now());
  25. }
  26. }
  27. handle_line(line) {
  28. const parts = line.split(' ');
  29. if (line == `:${Config.osu_username}[email protected] PART :${this.channel}`) {
  30. this.joined = false;
  31. db.prepare(`UPDATE match SET end_time = ? WHERE match_id = ?`).run(Date.now(), this.id);
  32. bancho._lobbies = bancho._lobbies.filter((lobby) => lobby.id != this.id);
  33. bancho.joined_lobbies = bancho.joined_lobbies.filter((lobby) => lobby.id != this.id);
  34. this.emit('close');
  35. return;
  36. }
  37. if (parts[1] == '332' && parts[3] == this.channel) {
  38. this.joined = true;
  39. this.invite_id = parseInt(parts[6].substring(1), 10);
  40. db.prepare(`UPDATE match SET invite_id = ? WHERE match_id = ?`).run(this.invite_id, this.id);
  41. bancho.emit('lobbyJoined', {
  42. channel: this.channel,
  43. lobby: this,
  44. });
  45. return;
  46. }
  47. if (parts[1] == 'PRIVMSG' && parts[2] == this.channel) {
  48. const full_source = parts.shift();
  49. parts.splice(0, 2);
  50. let source = null;
  51. if (full_source.indexOf('!') != -1) {
  52. source = full_source.substring(1, full_source.indexOf('!'));
  53. }
  54. const message = parts.join(' ').substring(1);
  55. if (source == 'BanchoBot') {
  56. let m;
  57. const joined_regex = /(.+) joined in slot \d+\./;
  58. const left_regex = /(.+) left the game\./;
  59. const room_name_regex = /Room name: (.+), History: https:\/\/osu\.ppy\.sh\/mp\/(\d+)/;
  60. const room_name_updated_regex = /Room name updated to "(.+)"/;
  61. const beatmap_regex = /Beatmap: https:\/\/osu\.ppy\.sh\/b\/(\d+) (.+)/;
  62. const mode_regex = /Team mode: (.+), Win condition: (.+)/;
  63. const mods_regex = /Active mods: (.+)/;
  64. const players_regex = /Players: (\d+)/;
  65. const score_regex = /(.+) finished playing \(Score: (\d+), (.+)\)\./;
  66. const slot_regex = /Slot (\d+) +(.+?) +https:\/\/osu\.ppy\.sh\/u\/(\d+) (.+)/;
  67. const ref_add_regex = /Added (.+) to the match referees/;
  68. const ref_del_regex = /Removed (.+) from the match referees/;
  69. const beatmap_change_regex = /Changed beatmap to https:\/\/osu\.ppy\.sh\/b\/(\d+) (.+)/;
  70. const player_changed_beatmap_regex = /Beatmap changed to: (.+) \(https:\/\/osu.ppy.sh\/b\/(\d+)\)/;
  71. const new_host_regex = /(.+) became the host./;
  72. if (message == 'Cleared match host') {
  73. this.host = null;
  74. this.emit('host');
  75. } else if (message == 'The match has started!') {
  76. this.scores = [];
  77. this.voteaborts = [];
  78. this.playing = true;
  79. this.emit('matchStarted');
  80. } else if (message == 'The match has finished!') {
  81. this.playing = false;
  82. // Used for !skip command
  83. for (const player of this.players) {
  84. if (!player.matches_finished) {
  85. player.matches_finished = 0;
  86. }
  87. player.matches_finished++;
  88. }
  89. this.emit('matchFinished');
  90. } else if (message == 'Aborted the match') {
  91. this.playing = false;
  92. this.emit('matchAborted');
  93. } else if (message == 'All players are ready') {
  94. this.emit('allPlayersReady');
  95. } else if (message == 'Changed the match password') {
  96. this.passworded = true;
  97. this.emit('password');
  98. } else if (message == 'Removed the match password') {
  99. this.passworded = false;
  100. this.emit('password');
  101. } else if (m = score_regex.exec(message)) {
  102. const score = {
  103. player: this.players.find((p) => p.irc_username == m[1]),
  104. score: parseInt(m[2], 10),
  105. };
  106. this.scores.push(score);
  107. this.emit('score', score);
  108. } else if (m = room_name_regex.exec(message)) {
  109. this.name = m[1];
  110. this.id = parseInt(m[2], 10);
  111. } else if (m = room_name_updated_regex.exec(message)) {
  112. this.name = m[1];
  113. db.prepare(`UPDATE match SET name = ? WHERE match_id = ?`).run(this.name, this.id);
  114. } else if (m = beatmap_regex.exec(message)) {
  115. this.map_data = null;
  116. this.beatmap_id = parseInt(m[1], 10);
  117. this.beatmap_name = m[2];
  118. } else if (m = beatmap_change_regex.exec(message)) {
  119. this.map_data = null;
  120. this.beatmap_id = parseInt(m[1], 10);
  121. this.beatmap_name = m[2];
  122. this.emit('refereeChangedBeatmap');
  123. } else if (m = player_changed_beatmap_regex.exec(message)) {
  124. this.map_data = null;
  125. this.beatmap_id = parseInt(m[2], 10);
  126. this.beatmap_name = m[1];
  127. this.emit('playerChangedBeatmap');
  128. } else if (m = mode_regex.exec(message)) {
  129. this.team_mode = m[1];
  130. this.win_condition = m[2];
  131. } else if (m = mods_regex.exec(message)) {
  132. this.active_mods = m[1];
  133. } else if (m = players_regex.exec(message)) {
  134. this.player_cache = this.players;
  135. this.players = [];
  136. this.players_to_parse = parseInt(m[1], 10);
  137. } else if (m = ref_add_regex.exec(message)) {
  138. this.emit('refereeAdded', m[1]);
  139. } else if (m = ref_del_regex.exec(message)) {
  140. if (m[1] == Config.osu_username) {
  141. this.leave();
  142. }
  143. this.emit('refereeRemoved', m[1]);
  144. } else if (m = slot_regex.exec(message)) {
  145. // !mp settings - single user result
  146. const update_player = (player) => {
  147. player.join_time = player.join_time || Date.now();
  148. player.irc_username = m[4].substring(0, 15).trimEnd();
  149. player.state = m[2];
  150. player.is_host = m[4].substring(16).indexOf('Host') != -1;
  151. if (player.is_host) {
  152. this.host = player;
  153. }
  154. this.players.push(player);
  155. this.players_to_parse--;
  156. if (this.players_to_parse == 0) {
  157. this.emit('settings');
  158. }
  159. };
  160. const cached_player = this.player_cache.find((p) => p.user_id == m[3]);
  161. if (cached_player) {
  162. update_player(cached_player);
  163. } else {
  164. get_or_create_user(parseInt(m[3], 10)).then(update_player);
  165. }
  166. } else if (m = new_host_regex.exec(message)) {
  167. // host changed
  168. for (const player of this.players) {
  169. player.is_host = player.irc_username == m[1];
  170. this.host = player;
  171. }
  172. this.emit('host');
  173. } else if (m = joined_regex.exec(message)) {
  174. // player joined
  175. const player = {
  176. join_time: Date.now(),
  177. total_scores: 0,
  178. irc_username: m[1],
  179. };
  180. this.players.push(player);
  181. this.emit('playerJoined', player);
  182. } else if (m = left_regex.exec(message)) {
  183. // player left
  184. const irc_username = m[1];
  185. const leaving_player = this.players.find((p) => p.irc_username == irc_username);
  186. if (leaving_player != null) {
  187. this.players = this.players.filter((player) => player.irc_username != irc_username);
  188. this.emit('playerLeft', leaving_player);
  189. }
  190. }
  191. return;
  192. }
  193. this.emit('message', {
  194. from: source,
  195. message: message,
  196. });
  197. for (const cmd of commands) {
  198. const match = cmd.regex.exec(message);
  199. if (!match) continue;
  200. if (!cmd.modes.includes('lobby')) break;
  201. if (cmd.creator_only) {
  202. const user_is_host = this.host && this.host.irc_username == source;
  203. let user_is_creator = false;
  204. for (const player of this.players) {
  205. if (player.irc_username == source) {
  206. user_is_creator = player.user_id == this.data.creator_id;
  207. break;
  208. }
  209. }
  210. if (!user_is_host && !user_is_creator) {
  211. this.send(`${source}: You need to be the lobby creator to use this command.`);
  212. break;
  213. }
  214. }
  215. cmd.handler({from: source, message: message}, match, this);
  216. break;
  217. }
  218. return;
  219. }
  220. }
  221. leave() {
  222. if (!this.joined) {
  223. return;
  224. }
  225. bancho._send('PART ' + this.channel);
  226. }
  227. async send(message) {
  228. if (!this.joined) {
  229. return;
  230. }
  231. return await bancho.privmsg(this.channel, message);
  232. }
  233. // Override EventEmitter to redirect errors to Sentry
  234. on(event_name, callback) {
  235. return super.on(event_name, (...args) => {
  236. try {
  237. Promise.resolve(callback(...args));
  238. } catch (err) {
  239. Sentry.setContext('lobby', {
  240. id: this.id,
  241. median_pp: this.median_overall,
  242. nb_players: this.players.length,
  243. data: this.data,
  244. task: event_name,
  245. });
  246. capture_sentry_exception(err);
  247. }
  248. });
  249. }
  250. }
  251. export {BanchoLobby};