(function($) { // `defaultserver` specifies the server to use when the domain name in the // address bar is `play.pokemonshowdown.com`. If the domain name in the // address bar is something else (including `dev.pokemonshowdown.com`), the // server to use will be determined by `crossdomain.php`, not this object. Config.defaultserver = { id: 'showdown', host: 'sim.smogon.com', port: 8000, altport: 80, registered: true }; Config.sockjsprefix = '/showdown'; Config.root = '/'; // sanitize a room ID // shouldn't actually do anything except against a malicious server var toRoomid = this.toRoomid = function(roomid) { return roomid.replace(/[^a-zA-Z0-9-]+/g, ''); } var User = this.User = Backbone.Model.extend({ defaults: { name: '', userid: '', registered: false, named: false, avatar: 0 }, /** * Return the path to the login server `action.php` file. AJAX requests * to this file will always be made on the `play.pokemonshowdown.com` * domain in order to have access to the correct cookies. */ getActionPHP: function() { var ret = '/~~' + Config.server.id + '/action.php'; if (Config.testclient) { ret = 'http://play.pokemonshowdown.com' + ret; } return (this.getActionPHP = function() { return ret; })(); }, /** * Process a signed assertion returned from the login server. * Emits the following events (arguments in brackets): * * `login:authrequried` (name) * triggered if the user needs to authenticate with this name * * `login:invalidname` (name, error) * triggered if the user's name is invalid * * `login:noresponse` * triggered if the login server did not return a response */ finishRename: function(name, assertion) { if (assertion === ';') { this.trigger('login:authrequried', name); } else if (assertion.substr(0, 2) === ';;') { this.trigger('login:invalidname', name, assertion.substr(2)); } else if (assertion.indexOf('\n') >= 0) { this.trigger('login:noresponse'); } else { app.send('/trn ' + name + ',0,' + assertion); } }, /** * Rename this user to an arbitrary username. If the username is * registered and the user does not currently have a session * associated with that userid, then the user will be required to * authenticate. * * See `finishRename` above for a list of events this can emit. */ rename: function(name) { if (this.userid !== toUserid(name)) { var query = this.getActionPHP() + '?act=getassertion&userid=' + encodeURIComponent(toUserid(name)) + '&challengekeyid=' + encodeURIComponent(this.challengekeyid) + '&challenge=' + encodeURIComponent(this.challenge); var self = this; $.get(query, function(data) { self.finishRename(name, data); }); } else { app.send('/trn ' + name); } }, challengekeyid: -1, challenge: '', receiveChallenge: function(attrs) { if (attrs.challenge) { /** * Rename the user based on the `sid` and `showdown_username` cookies. * Specifically, if the user has a valid session, the user will be * renamed to the username associated with that session. If the user * does not have a valid session but does have a persistent username * (i.e. a `showdown_username` cookie), the user will be renamed to * that name; if that name is registered, the user will be required * to authenticate. * * See `finishRename` above for a list of events this can emit. */ var query = this.getActionPHP() + '?act=upkeep' + '&challengekeyid=' + encodeURIComponent(attrs.challengekeyid) + '&challenge=' + encodeURIComponent(attrs.challenge); var self = this; $.get(query, Tools.safeJSON(function(data) { if (!data.username) return; if (data.loggedin) { self.set('registered', { username: data.username, userid: toUserid(data.username) }); } self.finishRename(data.username, data.assertion); }), 'text'); } this.challengekeyid = attrs.challengekeyid; this.challenge = attrs.challenge; }, /** * Log out from the server (but remain connected as a guest). */ logout: function() { $.post(this.getActionPHP(), { act: 'logout', userid: this.userid }); app.send('/logout'); }, setPersistentName: function(name) { $.cookie('showdown_username', (name !== undefined) ? name : this.get('name'), { expires: 14 }); }, /** * This function loads teams from `localStorage` or cookies. This function * is only used if the client is running on `play.pokemonshowdown.com`. If the * client is running on another domain (including `dev.pokemonshowdown.com`), * then teams are received from `crossdomain.php` instead. */ teams: null, loadTeams: function() { this.teams = []; if (window.localStorage) { var teamString = localStorage.getItem('showdown_teams'); if (teamString) this.teams = JSON.parse(teamString); } else { this.cookieTeams = true; var savedTeam = $.parseJSON($.cookie('showdown_team1')); if (savedTeam) { this.teams.push(savedTeam); } savedTeam = $.parseJSON($.cookie('showdown_team2')); if (savedTeam) { this.teams.push(savedTeam); } savedTeam = $.parseJSON($.cookie('showdown_team3')); if (savedTeam) { this.teams.push(savedTeam); } } } }); var App = this.App = Backbone.Router.extend({ root: '/', routes: { '*path': 'dispatchFragment' }, initialize: function() { window.app = this; $('#main').html(''); this.initializeRooms(); this.initializePopups(); this.user = new User(); this.topbar = new Topbar({el: $('#header')}); this.addRoom(''); this.on('init:unsupported', function() { alert('Your browser is unsupported.'); }); this.on('init:nothirdparty', function() { alert('You have third-party cookies disabled in your browser, which is likely to cause problems. You should enable them and then refresh this page.'); }); this.user.on('login:authrequried', function(name) { alert('The name ' + name + ' is registered, but you aren\'t logged in :('); }); this.initializeConnection(); Backbone.history.start({pushState: true}); }, /** * Start up the client, including loading teams and preferences, * determining which server to connect to, and actually establishing * a connection to that server. * * Triggers the following events (arguments in brackets): * `init:unsupported` * triggered if the user's browser is unsupported * * `init:loadteams` (teams) * triggered when loads are finished loading * * `init:nothirdparty` * triggered if the user has third-party cookies disabled and * third-party cookies/storage are necessary for full functioning * (i.e. stuff will probably be broken for the user, so show an * error message) * * `init:socketopened` * triggered once a socket has been opened to the sim server; this * does NOT mean that the user has signed in yet, merely that the * SockJS connection has been established. * * `init:connectionerror` * triggered if a connection to the sim server could not be * established * * `init:socketclosed` * triggered if the SockJS socket closes * * `init:identify` * triggered once the user has successfully identified (i.e. logged * in) with the server */ initializeConnection: function() { var origindomain = 'play.pokemonshowdown.com'; if (document.location.hostname === origindomain) { // TODO: BEFORE DEPLOYING THIS, add in code to check for use // of http://play.pokemonshowdown.com here. this.user.loadTeams(); this.trigger('init:loadteams'); Config.server = Config.defaultserver; return this.connect(); } else if (!window.postMessage) { // browser does not support cross-document messaging return this.trigger('init:unsupported'); } // If the URI in the address bar is not `play.pokemonshowdown.com`, // we receive teams, prefs, and server connection information from // crossdomain.php on play.pokemonshowdown.com. var self = this; $(window).on('message', (function() { var origin; var callbacks = {}; var callbackIdx = 0; return function($e) { var e = $e.originalEvent; if ((e.origin === 'http://' + origindomain) || (e.origin === 'https://' + origindomain)) { origin = e.origin; } else { return; // unauthorised source origin } var data = $.parseJSON(e.data); if (data.server) { var postCrossDomainMessage = function(data) { return e.source.postMessage($.toJSON(data), origin); }; // server config information Config.server = data.server; if (Config.server.registered) { var $link = $(''); $('head').append($link); } // persistent username self.user.setPersistentName = function() { postCrossDomainMessage({username: this.get('name')}); }; // ajax requests $.get = function(uri, callback, type) { var idx = callbackIdx++; callbacks[idx] = callback; postCrossDomainMessage({get: [uri, idx, type]}); }; $.post = function(uri, data, callback, type) { var idx = callbackIdx++; callbacks[idx] = callback; postCrossDomainMessage({post: [uri, data, idx, type]}); }; // teams if (data.teams) { self.user.cookieTeams = false; self.user.teams = $.parseJSON(data.teams); } else { self.user.teams = []; } TeambuilderRoom.saveTeams = function() { postCrossDomainMessage({teams: $.toJSON(app.user.teams)}); }; self.trigger('init:loadteams'); // prefs if (data.prefs) { Tools.prefs.data = $.parseJSON(data.prefs); } Tools.prefs.save = function() { postCrossDomainMessage({prefs: $.toJSON(this.data)}); }; // check for third-party cookies being disabled if (data.nothirdparty) { self.trigger('init:nothirdparty'); } // connect self.connect(); } else if (data.ajax) { var idx = data.ajax[0]; if (callbacks[idx]) { callbacks[idx](data.ajax[1]); delete callbacks[idx]; } } }; })()); // Note that the URI here is intentionally `play.pokemonshowdown.com`, // and not `dev.pokemonshowdown.com`, in order to make teams, prefs, // and other things work properly. var $iframe = $( '' ); $('body').append($iframe); }, /** * This function establishes the actual connection to the sim server. * This is intended to be called only by `initializeConnection` above. * Don't call this function directly. */ connect: function() { var self = this; var constructSocket = function() { var protocol = (Config.server.port === 443) ? 'https' : 'http'; return new SockJS(protocol + '://' + Config.server.host + ':' + Config.server.port + Config.sockjsprefix); }; this.socket = constructSocket(); /** * This object defines event handles for JSON-style messages. */ var events = { /** * These are all deprecated. Stop using them. :| */ init: function (data) { if (data.name !== undefined) { // Legacy self.user.set({ name: data.name, userid: toUserid(data.name), named: data.named }); } if (data.room) { // Correct way to initialize rooms: // >ROOMID // |init|ROOMTYPE // LOG if (data.room === 'lobby') { self.addRoom('lobby'); } else { self.joinRoom(data.room, data.roomType); } if (data.log) { self.rooms[data.room].add(data.log.join('\n')); } else if (data.battlelog) { self.rooms[data.room].init(data.battlelog.join('\n')); } } }, update: function (data) { if (data.name !== undefined) { // Legacy self.user.set({ name: data.name, userid: toUserid(data.name), named: data.named }); if (!data.named) { self.user.setPersistentName(null); // kill `showdown_username` cookie } } if (data.updates) { // Correct way to send battlelog updates: // >ROOMID // BATTLELOG var room = self.rooms[data.room]; if (room) room.receive(data.updates.join('\n')); } if ('challengesFrom' in data) { // Legacy if (self.rooms['']) self.rooms[''].updateChallenges(data); } if ('searching' in data) { // Legacy if (self.rooms['']) self.rooms[''].updateSearch(data); } if (data.request) { // Legacy var room = self.rooms[data.room]; if (room && room.receiveRequest) { if (data.request.side) data.request.side.id = data.side; room.receiveRequest(data.request); } } }, message: function (message) { // Correct way to send popups: (unimplemented) // |popup|MESSAGE alert(message.message); if (self.rooms['']) self.rooms[''].resetPending(); }, console: function (message) { if (message.pm) { // Correct way to send PMs: (unimplemented) // |pm|SOURCE|TARGET|MESSAGE self.rooms[''].addPM(message.name, message.message, message.pm); if (self.rooms['lobby']) { self.rooms['lobby'].addPM(message.name, message.message, message.pm); } } else if (message.rawMessage) { // Correct way to send raw console messages: // |raw|RAWMESSAGE self.receive('|raw|'+message.rawMessage); } else { // Correct way to send console messages: // MESSAGE self.receive(message.message); } }, disconnect: function () {}, nameTaken: function (data) {}, command: function (message) { // Legacy self.trigger('response:'+message.command, message); } }; var socketopened = false; var altport = (Config.server.port === Config.server.altport); var altprefix = false; this.socket.onopen = function() { socketopened = true; if (altport && window._gaq) { _gaq.push(['_trackEvent', 'Alt port connection', Config.server.id]); } self.trigger('init:socketopened'); // Join the lobby. This is necessary for now. // TODO: Revise this later if desired. self.send({room: 'lobby'}, 'join'); if (self.sendQueue) { var queue = self.sendQueue; delete self.sendQueue; for (var i=0; i') { var nlIndex = data.indexOf('\n'); if (nlIndex < 0) return; roomid = data.substr(1,nlIndex-1); data = data.substr(nlIndex+1); } if (roomid) { if (data.substr(0,6) === '|init|') { var roomType = data.substr(6); var roomTypeLFIndex = roomType.indexOf('\n'); if (roomTypeLFIndex >= 0) roomType = roomType.substr(0, roomTypeLFIndex); roomType = toId(roomType); if (roomid === 'lobby') { this.addRoom(roomid, roomType); } else { this.joinRoom(roomid, roomType); } } if (this.rooms[roomid]) { this.rooms[roomid].receive(data); } return; } var parts; if (data.charAt(0) === '|') { parts = data.substr(1).split('|'); } else { parts = []; } switch (parts[0]) { case 'challenge-string': case 'challstr': this.user.receiveChallenge({ challengekeyid: parseInt(parts[1], 10), challenge: parts[2] }); break; case 'formats': this.parseFormats(parts); break; default: if (data.substr(0,6) === '|init|') { this.addRoom('lobby'); } if (this.rooms['lobby']) { this.rooms['lobby'].receive(data); } break; } }, parseFormats: function(formatsList) { var isSection = false; var section = ''; BattleFormats = {}; for (var j=1; j