http = require 'http' Primus = require 'primus' Emitter = require('primus-emitter') express = require 'express' path = require 'path' {_} = require 'underscore' {BattleServer} = require './server' commands = require './commands' auth = require('./auth') generations = require './generations' {Room} = require('./rooms') errors = require '../shared/errors' assets = require('../assets') database = require('./database') redis = require('./redis') ratings = require('./ratings') config = require('./config') alts = require('./alts') replays = require('./replays') modify = require('./modify') randomTeam = require('./randomTeams') learnsets = require '../shared/learnsets' MAX_MESSAGE_LENGTH = 250 MAX_RANK_DISPLAYED = 25 # A MD5 hash of all the JavaScript files used by the client. This is passed to # each new connection via the .jade template, and when the client connects. If # the two versions differ, the server had restarted at some point and now is # serving new client files. CLIENT_VERSION = assets.getVersion() @createServer = (port) -> app = express() httpServer = http.createServer(app) primus = new Primus(httpServer, transformer: 'sockjs') primus.use('emitter', Emitter) primus.save(path.join(__dirname, "../client/vendor/js/primus.js")) server = new BattleServer() # Configuration app.set("views", "client/templates") app.set('view engine', 'jade') app.use(express.logger()) if config.IS_LOCAL app.use(express.compress()) # gzip app.use(express.cookieParser()) app.use(auth.middleware()) app.use(express.methodOverride()) app.use(app.router) app.use(express.static(path.join(__dirname, "../public"))) if config.IS_LOCAL # Helpers app.locals.asset_path = assets.getAbsolute # Routing renderHomepage = (req, res) -> res.render('index.jade', user: req.user, CLIENT_VERSION: CLIENT_VERSION) app.get("/", renderHomepage) app.get("/battles/:id", renderHomepage) app.get("/replays/:id", replays.routes.show) app.delete("/replays/:id", replays.routes.destroy) app.get("/replays", replays.routes.index) app.get "/pokemon/:id", (req, res) -> thispokemon = req.params.id thispokemon = thispokemon.replace("_", " ") pokeObj = {} pokeObj.name = thispokemon pokeObj.formedata = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].FormeData[thispokemon] pokeObj.speciesdata = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].SpeciesData[thispokemon] pokeObj.learnablemoves = learnsets.learnableMoves(generations.GenerationJSON, {species:thispokemon, forme:"default"}, 7) moveObj = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].MoveData res.render('pokemon.jade', data:pokeObj, move_data:moveObj) app.get "/pokemon/:id/json", (req, res) -> thispokemon = req.params.id thispokemon = thispokemon.replace("_", " ") pokeObj = {} pokeObj.name = thispokemon pokeObj.formedata = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].FormeData[thispokemon] pokeObj.speciesdata = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].SpeciesData[thispokemon] pokeObj.learnablemoves = learnsets.learnableMoves(generations.GenerationJSON, {species:thispokemon, forme:"default"}, 7) moveObj = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].MoveData res.json(data:pokeObj, move_data:moveObj) app.get '/leaderboard', (req, res) -> page = req.param('page') perPage = req.param('per_page') ratings.listRatings page, perPage, (err, results) -> temparr = results.map((e) -> e.username) ratioarr = [] ratings.getRatios temparr, (err, ratios)-> for username in temparr index = temparr.indexOf(username) results[index].ratio = ratios[username] if err res.json(500, err.message) else res.render('leaderboard.jade', players: results) app.get '/leaderboard/json', (req, res) -> page = req.param('page') perPage = req.param('per_page') ratings.listRatings page, perPage, (err, results) -> temparr = results.map((e) -> e.username) ratioarr = [] ratings.getRatios temparr, (err, ratios)-> for username in temparr index = temparr.indexOf(username) results[index].ratio = ratios[username] if err res.json(500, err.message) else res.json(players: results) app.get '/tiers', (req, res) -> formes = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].FormeData species = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].SpeciesData res.render('tiers.jade', formes: formes, species: species) app.get '/tiers/json', (req, res) -> formes = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].FormeData species = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].SpeciesData res.json(formes: formes) app.get '/ping', (req, res) -> res.json(200, {response: "pong"}) app.get '/pokeuse/json', (req, res) -> user= req.user res.json(400) if user.name != "Deukhoofd" page = req.param('page') perPage = req.param('per_page') q = new database.Teams() q = q.query('orderBy', 'created_at') .fetch() .then (teams) -> teams = teams.toJSON() pkmn = getpokeusedata(teams) res.json(teams: pkmn) getpokeusedata = (json) -> occurences = [] for team in json for pokemon in team.pokemon specie = pokemon.species if specie != null inarr = _.where(occurences, {name: specie}) if inarr.length == 0 newobj = {name: specie, occurence: 1} occurences.push(newobj) else inarr[0].occurence = inarr[0].occurence + 1 occurences = _.sortBy(occurences, "occurence").reverse() totalpokemon = 0 for pkmn in occurences totalpokemon += pkmn.occurence for pkmn in occurences pkmn.percentage = (pkmn.occurence/totalpokemon) * 100 return occurences lobby = new Room("Lobby") server.rooms.push(lobby) # Start responding to websocket clients primus.on 'connection', (spark) -> spark.send('version', CLIENT_VERSION) spark.on 'login', (id, token) -> return unless _.isFinite(id) return unless _.isString(token) requ = spark.request auth.matchToken requ, id, token, (err, json) -> if err return spark.send('errorMessage', errors.INVALID_SESSION) auth.getBanTTL json.name, (err, ttl) -> if err return spark.send('errorMessage', errors.INVALID_SESSION) else if ttl != -2 # -2 means the ban does not exist auth.getBanReason json.name, (err, reason) -> spark.send('errorMessage', errors.BANNED, reason, Number(ttl)) spark.end() return else user = server.findOrCreateUser(json, spark) if !user.name || !user.id console.error("MISSING INFORMATION: #{json}") spark.end() return attachEvents(user, spark) server.join(spark) spark.loggedIn = true spark.send('loginSuccess') lobby.add(spark) # After stuff alts.listUserAlts user.name, (err, alts) -> spark.send('altList', alts) primus.on 'error', (err) -> console.error(err.message, err.stack) primus.on 'disconnection', (spark) -> return unless spark.loggedIn server.leave(spark) spark.emit("cancelFindBattle") unless spark.user.hasSparks() attachEvents = (user, spark) -> spark.on 'sendChat', (roomId, message) -> return unless _.isString(message) message = message.trim().replace(/[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/g, '') return unless 0 < message.length < MAX_MESSAGE_LENGTH return unless room = server.getRoom(roomId) server.limit user, 'chat', max: 5, duration: 3000, (err, limit) -> if err || limit.remaining == 0 server.runIfUnmuted user, roomId, -> server.mute(user.name, '[AUTOMATED] Chat rate-limit.', 10 * 60) room = server.getRoom(roomId) room.announce('warning', "#{user.name} was automatically muted for 10 minutes.") else if message[0] == '/' && message[1] == '/' message = message[1...] server.userMessage(user, room, message) else if message[0] == '/' command = message.replace(/\s+.*$/, '') args = message.substr(command.length).replace(/^\s+/, '') command = command.substr(1) args = args.split(',') commands.executeCommand(server, user, room, command, args...) else server.userMessage(user, room, message) if limit.remaining == 1 user.announce(room.name, 'warning', 'You are chatting too fast. Please slow down.') spark.on 'leaveChatroom', (roomId) -> return unless _.isString(roomId) server.getRoom(roomId)?.remove(spark) ######### # TEAMS # ######### # Takes a temporary id and team JSON. Saves to server, and returns the real # unique id that was persisted onto the DB. spark.on 'saveTeam', (team, callback) -> return unless _.isObject(team) return unless _.isFunction(callback) attributes = _.pick(team, 'id', 'name', 'generation') attributes['trainer_id'] = user.id attributes['contents'] = JSON.stringify(team.pokemon) new database.Team(attributes) .save().then (team) -> callback(team.id) spark.on 'requestTeams', (fetchall = false) -> console.log(fetchall) q = new database.Teams() if !(fetchall && user.authority == auth.levels.OWNER) q = q.query('where', trainer_id: user.id) q = q.query('orderBy', 'created_at') .fetch() .then (teams) -> spark.send('receiveTeams', teams.toJSON()) spark.on 'destroyTeam', (teamId) -> return unless _.isFinite(teamId) attributes = { id: teamId } attributes['trainer_id'] = user.id unless config.IS_LOCAL database.Team.query().where(attributes).delete() .then -> # Do nothing, just execute the promise. We assume it was deleted. return .catch (err) -> console.error(err) spark.on 'getRandomTeamsAdmin', (format, number) -> if user.authority == auth.levels.OWNER teamArr = [] for [1..number] randomTeam.createTeamBookshelf format, [], (team) -> teamArr.push(team) teams = new database.Teams(teamArr) spark.send('receiveTeams', teams.toJSON()) #################### # PRIVATE MESSAGES # #################### spark.on 'privateMessage', (toUser, message) -> return unless _.isString(toUser) return unless _.isString(message) message = message.trim().replace(/[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/g, '') return unless 0 < message.length < MAX_MESSAGE_LENGTH if server.users.contains(toUser) recipient = server.users.get(toUser) recipient.send('privateMessage', user.name, user.name, message) user.send('privateMessage', toUser, user.name, message) else user.error(errors.PRIVATE_MESSAGE, toUser, "This user is offline.") ############## # CHALLENGES # ############## spark.on 'challenge', (challengeeId, generation, team, conditions, altName) -> return unless _.isString(challengeeId) return unless _.isString(generation) return unless _.isObject(team) return unless _.isArray(conditions) return unless !altName || _.isString(altName) alts.isAltOwnedBy user.name, altName, (err, valid) -> return user.error(errors.INVALID_ALT_NAME, "You do not own this alt") unless valid server.registerChallenge(user, challengeeId, generation, team, conditions, altName) spark.on 'cancelChallenge', (challengeeId) -> return unless _.isString(challengeeId) server.cancelChallenge(user, challengeeId) spark.on 'acceptChallenge', (challengerId, team, altName) -> return unless _.isString(challengerId) return unless _.isObject(team) return unless !altName || _.isString(altName) alts.isAltOwnedBy user.name, altName, (err, valid) -> return user.error(errors.INVALID_ALT_NAME, "You do not own this alt") unless valid server.acceptChallenge(user, challengerId, team, altName) spark.on 'rejectChallenge', (challengerId) -> return unless _.isString(challengerId) server.rejectChallenge(user, challengerId) ######## # ALTS # ######## spark.on 'createAlt', (altName) -> altName = String(altName).trim() if !alts.isAltNameValid(altName) return user.error(errors.INVALID_ALT_NAME, "Invalid Alt Name") alts.createAlt user.name, altName, (err, success) -> return user.error(errors.INVALID_ALT_NAME, err.message) if err user.send('altCreated', altName) if success ########### # REPLAYS # ########### spark.on 'saveReplay', (battleId, callback) -> battle = server.findBattle(battleId) return callback?("The battle could not be found.") unless battle return callback?("The battle is not yet done.") unless battle.isOver() replays.create(user, battle.battle) # unwrap the facade .then((replayId) -> callback?(null, replayId)) .catch replays.TooManyBattlesSaved, (err) -> callback?(err.message) .catch (err) -> callback?('Something went wrong saving the replay.') console.error(err.message) console.error(err.stack) ########### # BATTLES # ########### spark.on 'getBattleList', (callback) -> return unless _.isFunction(callback) # TODO: Make this more efficient # TODO: Order by age # NOTE: Cache this? Even something like a 5 second expiration # may improve server performance greatly currentTime = Date.now() battleMetadata = ([ controller.battle.id, controller.battle.playerNames[0], controller.battle.playerNames[1], currentTime - controller.battle.createdAt ] for controller in server.getOngoingBattles()) callback(battleMetadata) spark.on 'findBattle', (format, team, altName=null) -> return unless _.isString(format) return unless _.isObject(team) return unless !altName || _.isString(altName) # Note: If altName == null, then isAltOwnedBy will return true alts.isAltOwnedBy user.name, altName, (err, valid) -> if not valid user.error(errors.INVALID_ALT_NAME, "You do not own this alt") else validationErrors = server.queuePlayer(user.name, team, format, altName, "ranked") if validationErrors.length > 0 user.error(errors.FIND_BATTLE, validationErrors) spark.on 'cancelFindBattle', -> server.removePlayer(user.name) user.send("findBattleCanceled") spark.on 'findBattleunranked', (format, team, altName=null) -> return unless _.isString(format) return unless _.isObject(team) return unless !altName || _.isString(altName) # Note: If altName == null, then isAltOwnedBy will return true alts.isAltOwnedBy user.name, altName, (err, valid) -> if not valid user.error(errors.INVALID_ALT_NAME, "You do not own this alt") else validationErrors = server.queuePlayer(user.name, team, format, altName, "unranked") if validationErrors.length > 0 user.error(errors.FIND_BATTLE, validationErrors) spark.on 'cancelFindBattleunranked', -> server.removePlayerunranked(user.name) user.send("findBattleCanceledUnranked") spark.on 'findBattleRandom', (format, team, altName=null) -> return unless _.isString(format) return unless _.isObject(team) return unless !altName || _.isString(altName) # Note: If altName == null, then isAltOwnedBy will return true alts.isAltOwnedBy user.name, altName, (err, valid) -> if not valid user.error(errors.INVALID_ALT_NAME, "You do not own this alt") else validationErrors = server.queuePlayer(user.name, team, format, altName, "random") if validationErrors.length > 0 user.error(errors.FIND_BATTLE, validationErrors) spark.on 'cancelFindBattleRandom', -> server.removePlayerrandom(user.name) user.send("findBattleCanceledRandom") spark.on 'sendMove', (battleId, moveName, slot, forTurn, options, callback) -> return unless _.isString(moveName) return unless _.isFinite(slot) return unless _.isFinite(forTurn) return unless !options || _.isObject(options) return unless _.isFunction(callback) if battle = server.findBattle(battleId) battle.makeMove(user.name, moveName, slot, forTurn, options) callback() else user.error(errors.BATTLE_DNE, battleId) spark.on 'sendSwitch', (battleId, toSlot, fromSlot, forTurn, callback) -> return unless _.isFinite(toSlot) return unless _.isFinite(fromSlot) return unless _.isFinite(forTurn) return unless _.isFunction(callback) if battle = server.findBattle(battleId) battle.makeSwitch(user.name, toSlot, fromSlot, forTurn) callback() else user.error(errors.BATTLE_DNE, battleId) spark.on 'sendCancelAction', (battleId, forTurn) -> return unless _.isFinite(forTurn) if battle = server.findBattle(battleId) battle.undoCompletedRequest(user.name, forTurn) else user.error(errors.BATTLE_DNE, battleId) spark.on 'arrangeTeam', (battleId, arrangement) -> return unless _.isArray(arrangement) if battle = server.findBattle(battleId) battle.arrangeTeam(user.name, arrangement) else user.error(errors.BATTLE_DNE, battleId) spark.on 'spectateBattle', (battleId) -> if battle = server.findBattle(battleId) battle.add(spark) else user.error(errors.BATTLE_DNE, battleId) spark.on 'forfeit', (battleId) -> if battle = server.findBattle(battleId) battle.forfeit(user.name) else user.error(errors.BATTLE_DNE, battleId) battleSearch = -> server.beginBattles (err, battleIds) -> if err then return for id in battleIds battle = server.findBattle(id) playerIds = battle.getPlayerIds() ratingKeys = playerIds.map((id) -> battle.getPlayer(id).ratingKey) ratings.getRanks ratingKeys, (err, fullRanks) -> ranks = _.compact(fullRanks) return unless ranks.length == fullRanks.length if 1 <= Math.max(ranks...) <= MAX_RANK_DISPLAYED playerNames = battle.getPlayerNames() playerNames = playerNames.map((p, i) -> "#{p} (Rank ##{ranks[i]})") message = """A high-level match is being played! #{playerNames.join(" vs. ")}!""" lobby.message(message) setTimeout(battleSearch, 5 * 1000) battleSearchUnranked = -> server.beginBattlesunranked (err, battleIds) -> if err then return for id in battleIds battle = server.findBattle(id) playerIds = battle.getPlayerIds() ratingKeys = playerIds.map((id) -> battle.getPlayer(id).ratingKey) ratings.getRanks ratingKeys, (err, fullRanks) -> ranks = _.compact(fullRanks) setTimeout(battleSearchUnranked, 5 * 1000) battleSearchRandom = -> server.beginBattlesrandom (err, battleIds) -> if err then return for id in battleIds battle = server.findBattle(id) playerIds = battle.getPlayerIds() ratingKeys = playerIds.map((id) -> battle.getPlayer(id).ratingKey) ratings.getRanks ratingKeys, (err, fullRanks) -> ranks = _.compact(fullRanks) setTimeout(battleSearchRandom, 5 * 1000) battleSearch() battleSearchUnranked() battleSearchRandom() httpServer.listen(port) primus