123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- import EventEmitter from 'events';
- import {Socket} from 'net';
- import Config from './util/config.js';
- import {capture_sentry_exception} from './util/helpers.js';
- class BanchoClient extends EventEmitter {
- constructor() {
- super();
- this.connected = false;
- this._whois_requests = [];
- this._outgoing_messages = [];
- this._buffer = '';
- this._socket = null;
- this._writer = null;
- this._whoare = [];
- // Lobbies that we JOIN'd, not necessarily fully initialized
- this._lobbies = [];
- // Fully initialized lobbies where the bot is running actual logic
- this.joined_lobbies = [];
- }
- connect() {
- this._socket = new Socket();
- // Note sure if this is correct when IRC PING exists
- this._socket.setKeepAlive(true);
- this._socket.setTimeout(10000, () => {
- this._socket.emit('error', new Error('Timed out.'));
- });
- this._socket.on('error', (err) => {
- console.error('[IRC] Connection error:', err);
- capture_sentry_exception(err);
- this._socket.destroy();
- });
- this._socket.on('close', () => {
- console.info('[IRC] Connection closed. Cleaning up.');
- while (this._outgoing_messages.length > 0) {
- const obj = this._outgoing_messages.shift();
- obj.callback();
- }
- this._buffer = '';
- clearInterval(this._writer);
- this._writer = null;
- this.connected = false;
- this.emit('disconnect');
- });
- console.info('[IRC] Connecting...');
- this._socket.connect({
- host: 'irc.ppy.sh',
- port: 6667,
- }, () => {
- console.info('[IRC] Connected.');
- this._send(`PASS ${Config.osu_irc_password}`);
- this._send(`USER ${Config.osu_username} 0 * :${Config.osu_username}`);
- this._send(`NICK ${Config.osu_username}`);
- this._writer = setInterval(() => {
- const obj = this._outgoing_messages.shift();
- if (obj) {
- this._send(obj.message);
- obj.callback();
- }
- }, Config.MILLISECONDS_BETWEEN_SENDS);
- });
- return new Promise(async (resolve, reject) => {
- const {BanchoLobby} = await import('./lobby.js');
- this._socket.on('data', (data) => {
- data = data.toString().replace(/\r/g, '');
- this._buffer += data;
- const lines = this._buffer.split('\n');
- for (let i = 0; i < lines.length - 1; i++) {
- const parts = lines[i].split(' ');
- if (parts[1] != 'QUIT') {
- console.debug('< ' + lines[i]);
- }
- if (parts[0] == 'PING') {
- parts.shift();
- this._send('PONG ' + parts.join(' '));
- continue;
- }
- if (parts[1] == '001') {
- console.info('[IRC] Successfully logged in.');
- this.connected = true;
- resolve();
- continue;
- }
- if (parts[1] == '311') {
- // parts[3]: target username
- // parts[4]: user profile url
- const user_id = parseInt(parts[4].substring(parts[4].lastIndexOf('/') + 1), 10);
- if (parts[3] in this._whois_requests) {
- this._whoare[parts[3]] = user_id;
- this._whois_requests[parts[3]].resolve(user_id);
- delete this._whois_requests[parts[3]];
- }
- continue;
- }
- if (parts[1] == '401') {
- const target = parts[3];
- parts.splice(0, 4);
- if (target in this._whois_requests) {
- this._whois_requests[target].reject(parts.join(' ').substring(1));
- delete this._whois_requests[target];
- }
- continue;
- }
- // These channel-specific errors can be sent to a channel, even if
- // you haven't joined it or been forced to join it. :)
- const error_codes = ['461', '403', '405', '475', '474', '471', '473'];
- if (error_codes.includes(parts[1])) {
- const channel = parts[3];
- parts.splice(0, 4);
- this.emit(
- 'lobbyJoined', {
- channel: channel,
- lobby: null,
- },
- new Error(parts.join(' ').substring(1)),
- );
- continue;
- }
- if (parts[1] == '464') {
- console.error('[IRC] Invalid username/password. See: https://osu.ppy.sh/p/irc');
- parts.shift(); parts.shift();
- reject(new Error(parts.join(' ').substring(1)));
- continue;
- }
- // Bancho can push JOIN commands anytime, and we need to handle those appropriately.
- if (parts[1] == 'JOIN') {
- const channel = parts[2].substring(1);
- const lobby = new BanchoLobby(channel);
- this._lobbies.push(lobby);
- continue;
- }
- if (parts[1] == 'PRIVMSG' && parts[2] == Config.osu_username) {
- const full_source = parts.shift();
- parts.splice(0, 2);
- let source = null;
- if (full_source.indexOf('!') != -1) {
- source = full_source.substring(1, full_source.indexOf('!'));
- }
- const message = parts.join(' ').substring(1);
- this.emit('pm', {
- from: source,
- message: message,
- });
- continue;
- }
- // Unhandled line: pass it to all JOIN'd lobbies
- for (const lobby of this._lobbies) {
- lobby.handle_line(lines[i]);
- }
- }
- this._buffer = lines.pop();
- });
- });
- }
- _send(raw_message) {
- // Make sure users can't send commands by injecting '\r\n'
- raw_message = raw_message.split('\r')[0];
- raw_message = raw_message.split('\n')[0];
- if (this._socket && this._socket.readyState == 'open') {
- if (raw_message.indexOf('PASS ') == 0) {
- console.debug('> PASS *********');
- } else {
- console.debug('> ' + raw_message);
- }
- this._socket.write(raw_message + '\r\n');
- }
- }
- // The process for making lobbies goes like this:
- //
- // 1. You send !mp make <lobby title>
- // 2. Bancho makes you JOIN a new lobby
- // 3. Bancho sends you info about that lobby
- // 4. Bancho finally tells you the title and ID of the lobby
- //
- // Because of this, we automatically join lobbies that Bancho "pushes" onto
- // us, and wait for step 4 to resolve that auto-joined lobby.
- make(lobby_title) {
- return new Promise((resolve, reject) => {
- let nb_owned_lobbies = 0;
- for (const lobby of this._lobbies) {
- if (lobby.data.creator == Config.osu_username) {
- nb_owned_lobbies++;
- }
- }
- if (nb_owned_lobbies >= Config.max_lobbies) {
- return reject(new Error('Cannot create any more matches.'));
- }
- const room_created_listener = async (msg) => {
- const room_created_regex = /Created the tournament match https:\/\/osu\.ppy\.sh\/mp\/(\d+) (.+)/;
- if (msg.from == 'BanchoBot') {
- if (msg.message.indexOf('You cannot create any more tournament matches.') == 0) {
- this.off('pm', room_created_listener);
- return reject(new Error('Cannot create any more matches.'));
- }
- const m = room_created_regex.exec(msg.message);
- if (m && m[2] == lobby_title) {
- this.off('pm', room_created_listener);
- for (const lobby of this._lobbies) {
- if (lobby.id == m[1]) {
- console.log(`Created lobby "${lobby_title}".`);
- return resolve(lobby);
- }
- }
- // Should not be reachable, as long as Bancho sends the commands
- // in the right order (which it should).
- return reject(new Error('Lobby was created but not joined. (ask the devs to check the logs)'));
- }
- }
- };
- this.on('pm', room_created_listener);
- this.privmsg('BanchoBot', `!mp make ${lobby_title}`);
- });
- }
- join(channel) {
- return new Promise((resolve, reject) => {
- for (const lobby of this._lobbies) {
- if (lobby.channel == channel) {
- return reject(new Error('Lobby already joined'));
- }
- }
- const join_listener = (evt, err) => {
- if (evt.channel == channel) {
- this.off('lobbyJoined', join_listener);
- if (err) {
- return reject(err);
- } else {
- return resolve(evt.lobby);
- }
- }
- };
- this.on('lobbyJoined', join_listener);
- this._send('JOIN ' + channel);
- });
- }
- privmsg(destination, content) {
- // In a recent Bancho update, PMing users with spaces in their username
- // resulted in sometimes PMing the wrong user. To get around this, we
- // always replace spaces with underscores.
- destination = destination.replaceAll(' ', '_');
- return new Promise((resolve, reject) => {
- if (!this._socket || this._socket.readyState != 'open') {
- return resolve();
- }
- this._outgoing_messages.push({
- message: `PRIVMSG ${destination} :${content}`,
- callback: resolve,
- });
- });
- }
- // (async) Get a user ID from an IRC username.
- whois(irc_username) {
- return new Promise((resolve, reject) => {
- if (irc_username.indexOf(' ') != -1) {
- reject(new Error('Cannot WHOIS a username that contains spaces.'));
- return;
- }
- setTimeout(() => reject(new Error('Timeout out on whois request')), 5000);
- if (irc_username in this._whoare) {
- return resolve(this._whoare[irc_username]);
- }
- this._whois_requests[irc_username] = {resolve, reject};
- this._send('WHOIS ' + irc_username);
- });
- }
- }
- const bancho = new BanchoClient();
- export default bancho;
|