351 lines
13 KiB
CoffeeScript
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()
|