BattleSim/server/commands.coffee

351 lines
13 KiB
CoffeeScript

{_} = require('underscore')
async = require('async')
alts = require('./alts')
auth = require('./auth')
ratings = require('./ratings')
errors = require('../shared/errors')
exports.Commands = Commands = {}
exports.HelpDescriptions = HelpDescriptions = {}
desc = (description) ->
desc.lastDescription = description
parseArguments = (args) ->
args = Array::slice.call(args, 0)
hash = {}
if typeof args[args.length - 1] == 'function'
hash.callback = args.pop()
hash.args = args
hash
# Returns a 2-tuple, where the first element is the length (null for no length)
# and the second element is the reason.
parseLengthAndReason = (reasons) ->
length = null
return [null, ''] if reasons.length == 0
possibleLength = reasons[0].trim()
if /^[\dmshdyMw]+$/.test(possibleLength)
length = parseLength(possibleLength)
reasons = reasons[1...]
return [length, reasons.join(',').trim()]
parseLength = (length) ->
time = 0
for member in length.match(/\d+[mshdyMw]?/g)
first = parseInt(member, 10) # Truncates any letter after the number
last = member.substr(-1)
switch last
when 's'
time += first
when 'h'
time += first * 60 * 60
when 'd'
time += first * 60 * 60 * 24
break;
when 'w'
time += first * 60 * 60 * 24 * 7
when 'M'
time += first * 60 * 60 * 24 * 30
when 'y'
time += first * 60 * 60 * 24 * 30 * 12
else # minutes by default
time += first * 60
return time
prettyPrintTime = (seconds) ->
units = ["second", "minute", "hour", "day", "week", "month", "year"]
intervals = [60, 60, 24, 7, 4, 12, Infinity]
times = []
for interval, i in intervals
remainder = (seconds % interval)
seconds = Math.floor(seconds / interval)
unit = units[i]
unit += 's' if remainder != 1
times.push("#{remainder} #{unit}") if remainder > 0
break if seconds == 0
return times.reverse().join(", ")
makeCommand = (commandNames..., func) ->
authority = func.authority || auth.levels.USER
HelpDescriptions[authority] ?= {}
for commandName in commandNames
Commands[commandName] = func
# Generate description
description = ""
if commandNames.length > 1
aliases = commandNames[1...].map((n) -> "/#{n}").join(', ')
description += " <i>Also #{aliases}. </i>"
description += desc.lastDescription
HelpDescriptions[authority][commandNames[0]] = description
delete desc.lastDescription
makeModCommand = (commandNames..., func) ->
func.authority = auth.levels.MOD
makeCommand(commandNames..., func)
makeAdminCommand = (commandNames..., func) ->
func.authority = auth.levels.ADMIN
makeCommand(commandNames..., func)
makeOwnerCommand = (commandNames..., func) ->
func.authority = auth.levels.OWNER
makeCommand(commandNames..., func)
@executeCommand = (server, user, room, commandName, args...) ->
{args, callback} = parseArguments(args)
callback ||= ->
func = Commands[commandName]
if !func
message = "Invalid command: #{commandName}. Type /help to see a list."
user.error(errors.COMMAND_ERROR, room.name, message)
callback()
else if !func.authority || user.authority >= func.authority
Commands[commandName]?.call(server, user, room, callback, args...)
else
user.error(errors.COMMAND_ERROR, room.name, "You have insufficient authority.")
callback()
#######################
# Command definitions #
#######################
desc "Gets a single username's rating on this server. Usage: /rating username"
makeCommand "rating", "ranking", "rank", (user, room, next, username) ->
username ||= user.name
alts.getAltOwner username, (err, owner) ->
altKey = alts.uniqueId(owner, username)
commands = [
ratings.getRating.bind(ratings, username)
ratings.getRank.bind(ratings, username)
ratings.getRatio.bind(ratings, username)
]
commands.push(ratings.getRating.bind(ratings, altKey),
ratings.getRank.bind(ratings, altKey),
ratings.getRatio.bind(ratings, altKey)) if owner?
async.parallel commands, (err, results) ->
return user.error(errors.COMMAND_ERROR, room.name, err.message) if err
messages = []
messages.push collectRatingResults(username, results[...3], isOwner: username == user.name)
messages.push collectRatingResults("(Alt) #{username}", results[3...], isOwner: owner == user.name) if owner?
messages = _.compact(messages)
if messages.length == 0
user.announce(room.name, 'error', "Could not find rating for #{username}.")
else
user.announce(room.name, 'success', "#{messages.join('<br>')}")
next()
collectRatingResults = (username, results, options = {}) ->
isOwner = options.isOwner ? false
[rating, rank, ratios] = results
return if !rank
ratio = []
ratio.push("Rank: #{rank}")
ratio.push("Win: #{ratios.win}")
if isOwner
total = _.reduce(_.values(ratios), ((x, y) -> x + y), 0)
ratio.push("Lose: #{ratios.lose}")
ratio.push("Tie: #{ratios.draw}")
ratio.push("Total: #{total}")
"<b>#{username}'s rating:</b> #{rating} (#{ratio.join(' / ')})"
desc "Finds all the battles a username is playing in on this server.
Usage: /battles username"
makeCommand "battles", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /battles username")
return next()
battleIds = @getVisibleUserBattles(username)
links = battleIds.map (id) ->
"<span class='fake_link spectate' data-battle-id='#{id}'>#{id[...6]}</span>"
message = if battleIds.length == 0
"#{username} is not playing any battles."
else
"#{username}'s battles: #{links.join(" | ")}"
user.announce(room.name, 'success', message)
next()
desc "Default length is 10 minutes, up to a maximum of two days. To specify different lengths, use 1m2h3d4w (minute, hour, day, week). Usage: /mute username, length, reason"
makeModCommand "mute", (user, room, next, username, reason...) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /mute username, length, reason")
return next()
[length, reason] = parseLengthAndReason(reason)
# Enforce a length for non-admins.
if user.authority < auth.levels.ADMIN
length = 10 * 60 if !length? || length <= 0
length = Math.min(parseLength("2d"), length) # max of two days
@mute(username, reason, length)
message = "#{user.name} muted #{username} for #{prettyPrintTime(length)}"
message += " (#{reason})" if reason.length > 0
room.announce('warning', message)
next()
desc "Unmutes a username. Usage: /unmute username"
makeModCommand "unmute", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /unmute username")
return next()
auth.getMuteTTL username, (err, ttl) =>
if ttl == -2
user.error(errors.COMMAND_ERROR, room.name, "#{username} is already unmuted!")
return next()
else
@unmute(username)
message = "#{user.name} unmuted #{username}"
room.announce('warning', message)
next()
desc "Default length is one hour, up to a maximum of one day. To specify different lengths, use 1m2h3d (minute, hour, day). Usage: /ban username, length, reason"
makeModCommand "ban", (user, room, next, username, reason...) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /ban username, length, reason")
return next()
[length, reason] = parseLengthAndReason(reason)
# Enforce a length for non-admins
if user.authority < auth.levels.ADMIN
length = 60 * 60 if !length? || length <= 0
length = Math.min(parseLength("1d"), length) # max of one day
@ban(username, reason, length)
message = "#{user.name} banned #{username}"
message += " for #{prettyPrintTime(length)}" if length
message += " (#{reason})" if reason.length > 0
room.announce('warning', message)
next()
desc "Unbans a username. Usage: /unban username"
makeModCommand "unban", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /unban username")
return next()
auth.getBanTTL username, (err, ttl) =>
if ttl == -2
user.error(errors.COMMAND_ERROR, room.name, "#{username} is already unbanned!")
return next()
else
@unban username, =>
message = "#{user.name} unbanned #{username}"
room.announce('warning', message)
return next()
desc "Finds the current ips under use by a user"
makeModCommand "ip", (user, room, next, nameOrIp) ->
if !nameOrIp
user.error(errors.COMMAND_ERROR, "Usage: /ip username")
return next()
checkedUser = @users.get(nameOrIp)
if checkedUser
ips = checkedUser.sparks.map((spark) -> spark.address.ip)
ips = _.chain(ips).compact().unique().value()
user.announce(room.name, 'success', "#{nameOrIp}'s IP addresses: #{ips.join(', ')}")
else
users = []
for checkedUser in @users.getUsers()
for spark in checkedUser.sparks
if spark.address.ip == nameOrIp
users.push(checkedUser.name)
break
user.announce(room.name, 'success', "Users with IP #{nameOrIp}: #{users.join(', ')}")
next()
desc "Prevents new battles from starting. Usage: /lockdown [on|off]"
makeAdminCommand "lockdown", (user, room, next, option = "on") ->
if option not in [ "on", "off" ]
user.error(errors.COMMAND_ERROR, room.name, "Usage: /lockdown [on|off]")
return next()
if option == 'on' then @lockdown() else @unlockdown()
next()
desc "Voices a username permanently. Usage: /voice username"
makeAdminCommand "voice", "driver", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /voice username")
return next()
auth.setAuth username, auth.levels.DRIVER, (err, result) =>
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
@setAuthority(username, auth.levels.DRIVER)
return next()
desc "Mods a username permanently. Usage: /mod username"
makeAdminCommand "mod", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /mod username")
return next()
auth.setAuth username, auth.levels.MOD, (err, result) =>
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
@setAuthority(username, auth.levels.MOD)
return next()
desc "Admins a username permanently. Usage: /admin username"
makeOwnerCommand "admin", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /admin username")
return next()
auth.setAuth username, auth.levels.ADMIN, (err, result) =>
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
@setAuthority(username, auth.levels.ADMIN)
return next()
desc "Deauthes a username permanently. Usage: /deauth username"
makeOwnerCommand "deauth", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /deauth username")
return next()
auth.setAuth username, auth.levels.USER, (err, result) =>
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
@setAuthority(username, auth.levels.USER)
return next()
desc "Changes the topic message. Usage: /topic message"
makeAdminCommand "topic", (user, room, next, topicPieces...) ->
room.setTopic(topicPieces.join(','))
next()
desc "Announces something to the entire server. Usage: /wall message"
makeModCommand "wall", "announce", (user, room, next, pieces...) ->
message = pieces.join(',')
return next() if !message
@announce("<strong>#{user.name}:</strong> #{message}")
next()
desc "Finds all alts associated with a username, or the main username of an alt"
makeModCommand "whois", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /whois username")
return next()
messages = []
alts.getAltOwner username, (err, ownerName) ->
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
ownerName ?= username
messages.push("<b>Main account:</b> #{ownerName}")
alts.listUserAlts username, (err, alts) ->
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
messages.push("<b>Alts:</b> #{alts.join(', ')}") if alts.length > 0
user.announce(room.name, 'success', messages.join(' | '))
return next()
desc "Evaluates a script in the context of the server."
makeOwnerCommand "eval", (user, room, next, pieces...) ->
source = pieces.join(',')
return next() if !source
try
result = (new Function("with(this) { return #{source} }")).call(this)
user.announce(room.name, 'success', "> #{result}")
catch e
user.error(errors.COMMAND_ERROR, room.name, "EVAL ERROR: #{e.message}")
next()