1660 lines
62 KiB
Lua
1660 lines
62 KiB
Lua
verbana.commands = {}
|
|
|
|
local data = verbana.data
|
|
local lib_asn = verbana.lib_asn
|
|
local lib_ip = verbana.lib_ip
|
|
local log = verbana.log
|
|
local settings = verbana.settings
|
|
local util = verbana.util
|
|
|
|
local mod_priv = verbana.privs.moderator
|
|
local admin_priv = verbana.privs.admin
|
|
local kick_priv = verbana.privs.kick
|
|
local debug_mode = settings.debug_mode
|
|
|
|
local parse_timespan = util.parse_timespan
|
|
local safe = util.safe
|
|
local safe_kick_player = util.safe_kick_player
|
|
local table_contains = util.table_contains
|
|
local iso_date = util.iso_date
|
|
|
|
local worldpath = minetest.get_worldpath()
|
|
|
|
local function chat_send_player(name, message, ...)
|
|
message = message:format(...)
|
|
minetest.chat_send_player(name, message)
|
|
local irc_message = minetest.strip_colors(message)
|
|
if minetest.global_exists('irc') and irc.joined_players[name] then
|
|
irc.say(name, irc_message)
|
|
end
|
|
if minetest.global_exists('irc2') and irc2.joined_players[name] then
|
|
irc2.say(name, irc_message)
|
|
end
|
|
end
|
|
|
|
local function register_chatcommand(name, def)
|
|
if debug_mode then name = ('v_%s'):format(name) end
|
|
def.func = safe(def.func)
|
|
minetest.register_chatcommand(name, def)
|
|
end
|
|
|
|
local function override_chatcommand(name, def)
|
|
def.func = safe(def.func)
|
|
if debug_mode then
|
|
name = ('v_%s'):format(name)
|
|
minetest.register_chatcommand(name, def)
|
|
else
|
|
minetest.override_chatcommand(name, def)
|
|
end
|
|
end
|
|
|
|
local function alias_chatcommand(name, existing_name)
|
|
if debug_mode then
|
|
name = ('v_%s'):format(name)
|
|
existing_name = ('v_%s'):format(existing_name)
|
|
end
|
|
local existing_def = minetest.registered_chatcommands[existing_name]
|
|
if not existing_def then
|
|
log('error', "Could not alias command %q to %q, because %q doesn't exist", name, existing_name, existing_name)
|
|
else
|
|
minetest.register_chatcommand(name, existing_def)
|
|
end
|
|
|
|
end
|
|
|
|
register_chatcommand('sban_import', {
|
|
description='Import records from sban into verbana',
|
|
params='[<filename>]',
|
|
privs={[admin_priv]=true},
|
|
func=function (caller, filename)
|
|
if not filename or filename == '' then
|
|
filename = worldpath .. '/sban.sqlite'
|
|
end
|
|
if not util.file_exists(filename) then
|
|
return false, ('Could not open file %q.'):format(filename)
|
|
end
|
|
chat_send_player(caller, 'Importing SBAN. This can take a while...')
|
|
if data.import_from_sban(filename) then
|
|
return true, 'Successfully imported.'
|
|
else
|
|
return false, 'Error importing SBAN db (see server log)'
|
|
end
|
|
end
|
|
})
|
|
|
|
register_chatcommand('verification', {
|
|
description='Turn universal verification on or off',
|
|
params='on | off',
|
|
privs={[admin_priv]=true},
|
|
func=function(caller, params)
|
|
local value
|
|
if params == 'on' then
|
|
value = true
|
|
elseif params == 'off' then
|
|
value = false
|
|
else
|
|
return false, 'Invalid parameters'
|
|
end
|
|
if settings.universal_verification == value then
|
|
return true, ('Universal verification is already %s'):format(params)
|
|
end
|
|
settings.set_universal_verification(value)
|
|
return true, ('Turned universal verification %s'):format(params)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('asn', {
|
|
description='Get the ASN associated with an IP or player name',
|
|
params='<player_name> | <IP>',
|
|
privs={[mod_priv]=true},
|
|
func = function(_, name_or_ipstr)
|
|
local ipstr
|
|
|
|
if lib_ip.is_valid_ip(name_or_ipstr) then
|
|
ipstr = name_or_ipstr
|
|
else
|
|
ipstr = data.fumble_about_for_an_ip(name_or_ipstr)
|
|
end
|
|
|
|
if not ipstr or ipstr == '' then
|
|
return false, ('"%s" is not a valid ip nor a known player'):format(name_or_ipstr)
|
|
end
|
|
|
|
local asn, description = lib_asn.lookup(ipstr)
|
|
if not asn or asn == 0 then
|
|
return false, ('could not find ASN for "%s"'):format(ipstr)
|
|
end
|
|
|
|
description = description or ''
|
|
|
|
return true, ('A%u (%s)'):format(asn, description)
|
|
end
|
|
})
|
|
----------------- SET PLAYER STATUS COMMANDS -----------------
|
|
local function parse_player_status_params(params)
|
|
local name, reason = params:match('^([a-zA-Z0-9_-]+)%s+(.*)$')
|
|
if not name then
|
|
name = params:match('^([a-zA-Z0-9_-]+)$')
|
|
end
|
|
if not name or name:len() > 20 then
|
|
return nil, nil, nil, ('Invalid argument(s): %q'):format(params)
|
|
end
|
|
local player_id, player_name = data.get_player_id(name)
|
|
if not player_id then
|
|
return nil, nil, nil, ('Unknown player: %s'):format(name)
|
|
end
|
|
local player_status = data.get_player_status(player_id, true)
|
|
return player_id, player_name, player_status, reason
|
|
end
|
|
|
|
local function has_suspicious_connection(player_id)
|
|
local connection_log = data.get_player_connection_log(player_id, 1)
|
|
if not connection_log or #connection_log ~= 1 then
|
|
log('warning', 'player %s exists but has no connection log?', player_id)
|
|
return true
|
|
end
|
|
local last_login = connection_log[1]
|
|
if last_login.ip_status_id == data.ip_status.trusted.id then
|
|
return false
|
|
elseif last_login.ip_status_id and last_login.ip_status_id ~= data.ip_status.default.id then
|
|
return true
|
|
elseif last_login.asn_status_id == data.asn_status.default.id then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
register_chatcommand('verify', {
|
|
description='Verify a player',
|
|
params='<player_name> [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local player_id, player_name, player_status, reason = parse_player_status_params(params)
|
|
if not player_id then
|
|
return false, reason
|
|
end
|
|
if player_status.id ~= data.player_status.unverified.id then
|
|
return false, ('Player %s is not unverified'):format(player_name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
local status_id
|
|
if has_suspicious_connection(player_id) then
|
|
status_id = data.player_status.suspicious.id
|
|
else
|
|
status_id = data.player_status.default.id
|
|
end
|
|
if not data.set_player_status(player_id, executor_id, status_id, reason) then
|
|
return false, 'ERROR setting player status'
|
|
end
|
|
log('action', 'setting verified privs for %s', player_name)
|
|
if not debug_mode then
|
|
minetest.set_player_privs(player_name, settings.verified_privs)
|
|
end
|
|
local player = minetest.get_player_by_name(player_name)
|
|
if player then
|
|
log('action', 'moving %s to spawn', player_name)
|
|
if not debug_mode then
|
|
player:set_pos(settings.spawn_pos)
|
|
end
|
|
end
|
|
if reason then
|
|
log('action', '%s verified %s because %s', caller, player_name, reason)
|
|
else
|
|
log('action', '%s verified %s', caller, player_name)
|
|
end
|
|
return true, ('Verified %s'):format(player_name)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('unverify', {
|
|
description='Unverify a player, revoking privs and putting them back in jail.',
|
|
params='<player_name> [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local player_id, player_name, player_status, reason = parse_player_status_params(params)
|
|
if not player_id then
|
|
return false, reason
|
|
end
|
|
if not table_contains({
|
|
data.player_status.default.id,
|
|
data.player_status.suspicious.id,
|
|
}, player_status.id) then
|
|
return false, ('Cannot unverify %s w/ status %s'):format(player_name, player_status.name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_player_status(player_id, executor_id, data.player_status.unverified.id, reason) then
|
|
return false, 'ERROR setting player status'
|
|
end
|
|
log('action', 'setting unverified privs for %s', player_name)
|
|
if not debug_mode then
|
|
minetest.set_player_privs(player_name, settings.unverified_privs)
|
|
end
|
|
local player = minetest.get_player_by_name(player_name)
|
|
if player then
|
|
log('action', 'moving %s to unverified area', player_name)
|
|
if not debug_mode then
|
|
player:set_pos(settings.unverified_spawn_pos)
|
|
end
|
|
end
|
|
if reason then
|
|
log('action', '%s unverified %s because %s', caller, player_name, reason)
|
|
else
|
|
log('action', '%s unverified %s', caller, player_name)
|
|
end
|
|
return true, ('Unverified %s'):format(player_name)
|
|
end
|
|
})
|
|
|
|
local kick_privs = {[mod_priv]=true}
|
|
if kick_priv and kick_priv ~= mod_priv then
|
|
kick_privs = nil
|
|
end
|
|
|
|
override_chatcommand('kick', {
|
|
description='Kick a player',
|
|
params='<player_name> [<reason>]',
|
|
privs=kick_privs or {},
|
|
func=function(caller, params)
|
|
if not kick_privs then
|
|
if not (minetest.check_player_privs(caller, {[mod_priv]=true}) or
|
|
minetest.check_player_privs(caller, {[kick_priv]=true})) then
|
|
return false, "You lack sufficient privileges to run this command"
|
|
end
|
|
end
|
|
local player_id, player_name, _, reason = parse_player_status_params(params)
|
|
if not player_id then
|
|
return false, reason
|
|
end
|
|
local player = minetest.get_player_by_name(player_name)
|
|
if not player then
|
|
return false, ("Player %s not in game!"):format(player_name)
|
|
end
|
|
safe_kick_player(caller, player, reason)
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_player_status(player_id, executor_id, data.player_status.kicked.id, reason, nil, true) then
|
|
return false, 'ERROR logging player status'
|
|
end
|
|
if reason then
|
|
log('action', '%s kicked %s because %s', caller, player_name, reason)
|
|
else
|
|
log('action', '%s kicked %s', caller, player_name)
|
|
end
|
|
return true, ('Kicked %s'):format(player_name)
|
|
end
|
|
})
|
|
|
|
override_chatcommand('ban', {
|
|
description='Ban a player. Timespan and reason are optional.',
|
|
params='<player_name> [<timespan>] [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local player_id, player_name, player_status, reason = parse_player_status_params(params)
|
|
if not player_id then
|
|
return false, reason
|
|
end
|
|
local now = os.time()
|
|
local timespan
|
|
local expires
|
|
if reason then
|
|
local first = reason:match('^(%S+)')
|
|
timespan = parse_timespan(first)
|
|
if timespan then
|
|
reason = reason:sub(first:len() + 2)
|
|
expires = now + timespan
|
|
end
|
|
end
|
|
if not table_contains({
|
|
data.player_status.default.id,
|
|
data.player_status.unverified.id,
|
|
data.player_status.suspicious.id,
|
|
}, player_status.id) then
|
|
return false, ('Cannot ban %s w/ status %s'):format(player_name, player_status.name)
|
|
end
|
|
local player = minetest.get_player_by_name(player_name)
|
|
if player then
|
|
safe_kick_player(caller, player, reason)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_player_status(player_id, executor_id, data.player_status.banned.id, reason, expires) then
|
|
return false, 'ERROR setting player status'
|
|
end
|
|
if expires then
|
|
if reason then
|
|
log('action', '%s banned %s until %s because %s', caller, player_name, iso_date(expires), reason)
|
|
else
|
|
log('action', '%s banned %s until %s', caller, player_name, iso_date(expires))
|
|
end
|
|
else
|
|
if reason then
|
|
log('action', '%s banned %s because %s', caller, player_name, reason)
|
|
else
|
|
log('action', '%s banned %s', caller, player_name)
|
|
end
|
|
end
|
|
local ipstr = data.fumble_about_for_an_ip(player_name)
|
|
if ipstr then
|
|
local ipint = lib_ip.ipstr_to_ipint(ipstr)
|
|
local ip_status = data.get_ip_status(ipint)
|
|
if not ip_status or ip_status.name == data.ip_status.default.name then
|
|
-- mark the IP the player is on as suspicious
|
|
local ip_suspect_expires
|
|
if expires then
|
|
ip_suspect_expires = expires
|
|
else
|
|
ip_suspect_expires = now + parse_timespan('1w')
|
|
end
|
|
local ip_suspect_reason = ('Marked suspicious while banning %s'):format(player_name)
|
|
if not data.set_ip_status(ipint, executor_id, data.ip_status.suspicious.id, ip_suspect_reason, ip_suspect_expires) then
|
|
return false, 'ERROR setting ip status'
|
|
end
|
|
end
|
|
end
|
|
return true, ('Banned %s'):format(player_name)
|
|
end
|
|
})
|
|
|
|
override_chatcommand('unban', {
|
|
description='Unban a player',
|
|
params='<player_name> [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local player_id, player_name, player_status, reason = parse_player_status_params(params)
|
|
if not player_id then
|
|
return false, reason
|
|
end
|
|
if player_status.id ~= data.player_status.banned.id then
|
|
return false, ('Player %s is not banned!'):format(player_name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
local status_id
|
|
if has_suspicious_connection(player_id) then
|
|
status_id = data.player_status.suspicious.id
|
|
else
|
|
status_id = data.player_status.default.id
|
|
end
|
|
if not data.set_player_status(player_id, executor_id, status_id, reason) then
|
|
return false, 'ERROR setting player status'
|
|
end
|
|
if reason then
|
|
log('action', '%s unbanned %s because %s', caller, player_name, reason)
|
|
else
|
|
log('action', '%s unbanned %s', caller, player_name)
|
|
end
|
|
return true, ('Unbanned %s'):format(player_name)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('whitelist', {
|
|
description='Whitelist a player',
|
|
params='<player_name> [<reason>]',
|
|
privs={[admin_priv]=true},
|
|
func=function(caller, params)
|
|
local player_id, player_name, player_status, reason = parse_player_status_params(params)
|
|
if not player_id then
|
|
return false, reason
|
|
end
|
|
if not table_contains({
|
|
data.player_status.default.id,
|
|
data.player_status.unverified.id,
|
|
data.player_status.suspicious.id,
|
|
}, player_status.id) then
|
|
return false, ('Cannot whitelist %s w/ status %s'):format(player_name, player_status.name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_player_status(player_id, executor_id, data.player_status.whitelisted.id, reason) then
|
|
return false, 'ERROR setting player status'
|
|
end
|
|
if reason then
|
|
log('action', '%s whitelisted %s because %s', caller, player_name, reason)
|
|
else
|
|
log('action', '%s whitelisted %s', caller, player_name)
|
|
end
|
|
return true, ('Whitelisted %s'):format(player_name)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('unwhitelist', {
|
|
description='Remove whitelist status from a player',
|
|
params='<player_name> [<reason>]',
|
|
privs={[admin_priv]=true},
|
|
func=function(caller, params)
|
|
local player_id, player_name, player_status, reason = parse_player_status_params(params)
|
|
if not player_id then
|
|
return false, reason
|
|
end
|
|
if player_status.id ~= data.player_status.whitelisted.id then
|
|
return false, ('Player %s is not whitelisted!'):format(player_name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_player_status(player_id, executor_id, data.player_status.default.id, reason) then
|
|
return false, 'ERROR setting player status'
|
|
end
|
|
if reason then
|
|
log('action', '%s unwhitelisted %s because %s', caller, player_name, reason)
|
|
else
|
|
log('action', '%s unwhitelisted %s', caller, player_name)
|
|
end
|
|
return true, ('Unwhitelisted %s'):format(player_name)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('suspect', {
|
|
description='Mark a player as suspicious',
|
|
params='<player_name> [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local player_id, player_name, player_status, reason = parse_player_status_params(params)
|
|
if not player_id then
|
|
return false, reason
|
|
end
|
|
if player_status.id ~= data.player_status.default.id then
|
|
return false, ('Cannot whitelist %s w/ status %s'):format(player_name, player_status.name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_player_status(player_id, executor_id, data.player_status.suspicious.id, reason) then
|
|
return false, 'ERROR setting player status'
|
|
end
|
|
if reason then
|
|
log('action', '%s suspected %s because %s', caller, player_name, reason)
|
|
else
|
|
log('action', '%s suspected %s', caller, player_name)
|
|
end
|
|
return true, ('Suspected %s'):format(player_name)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('unsuspect', {
|
|
description='Unmark a player as suspicious',
|
|
params='<player_name> [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local player_id, player_name, player_status, reason = parse_player_status_params(params)
|
|
if not player_id then
|
|
return false, reason
|
|
end
|
|
if player_status.id ~= data.player_status.suspicious.id then
|
|
return false, ('Player %s is not suspicious!'):format(player_name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_player_status(player_id, executor_id, data.player_status.default.id, reason) then
|
|
return false, 'ERROR setting player status'
|
|
end
|
|
if reason then
|
|
log('action', '%s unsuspected %s because %s', caller, player_name, reason)
|
|
else
|
|
log('action', '%s unsuspected %s', caller, player_name)
|
|
end
|
|
return true, ('Unsuspected %s'):format(player_name)
|
|
end
|
|
})
|
|
----------------- SET IP STATUS COMMANDS -----------------
|
|
local function parse_ip_status_params(params)
|
|
local ipstr, reason = params:match('^(%d+%.%d+%.%d+%.%d+)%s+(.*)$')
|
|
if not ipstr then
|
|
ipstr = params:match('^(%d+%.%d+%.%d+%.%d+)$')
|
|
end
|
|
if not ipstr or not lib_ip.is_valid_ip(ipstr) then
|
|
return nil, nil, nil, ('Invalid argument(s): %q'):format(params)
|
|
end
|
|
local ipint = lib_ip.ipstr_to_ipint(ipstr)
|
|
data.register_ip(ipint)
|
|
local ip_status = data.get_ip_status(ipint, true)
|
|
return ipint, ipstr, ip_status, reason
|
|
end
|
|
|
|
register_chatcommand('ip_trust', {
|
|
description='Mark an IP as trusted - connections will bypass suspicious network checks',
|
|
params='<IP> [<reason>]',
|
|
privs={[admin_priv]=true},
|
|
func=function(caller, params)
|
|
local ipint, ipstr, ip_status, reason = parse_ip_status_params(params)
|
|
if not ipint then
|
|
return false, reason
|
|
end
|
|
if not table_contains({
|
|
data.ip_status.default.id,
|
|
data.ip_status.suspicious.id,
|
|
}, ip_status.id) then
|
|
return false, ('Cannot trust IP w/ status %s'):format(ip_status.name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_ip_status(ipint, executor_id, data.ip_status.trusted.id, reason) then
|
|
return false, 'ERROR setting IP status'
|
|
end
|
|
if reason then
|
|
log('action', '%s trusted %s because %s', caller, ipstr, reason)
|
|
else
|
|
log('action', '%s trusted %s', caller, ipstr)
|
|
end
|
|
return true, ('Trusted %s'):format(ipstr)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('ip_untrust', {
|
|
description='Remove trusted status from an IP',
|
|
params='<IP> [<reason>]',
|
|
privs={[admin_priv]=true},
|
|
func=function(caller, params)
|
|
local ipint, ipstr, ip_status, reason = parse_ip_status_params(params)
|
|
if not ipint then
|
|
return false, reason
|
|
end
|
|
if ip_status.id ~= data.player_status.trusted.id then
|
|
return false, ('IP %s is not trusted!'):format(ipstr)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_ip_status(ipint, executor_id, data.ip_status.default.id, reason) then
|
|
return false, 'ERROR setting IP status'
|
|
end
|
|
if reason then
|
|
log('action', '%s untrusted %s because %s', caller, ipstr, reason)
|
|
else
|
|
log('action', '%s untrusted %s', caller, ipstr)
|
|
end
|
|
return true, ('Untrusted %s'):format(ipstr)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('ip_suspect', {
|
|
description='Mark an IP as suspicious.',
|
|
params='<IP> [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local ipint, ipstr, ip_status, reason = parse_ip_status_params(params)
|
|
if not ipint then
|
|
return false, reason
|
|
end
|
|
if ip_status.id ~= data.ip_status.default.id then
|
|
return false, ('Cannot suspect IP w/ status %s'):format(ip_status.name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_ip_status(ipint, executor_id, data.ip_status.suspicious.id, reason) then
|
|
return false, 'ERROR setting IP status'
|
|
end
|
|
if reason then
|
|
log('action', '%s suspected %s because %s', caller, ipstr, reason)
|
|
else
|
|
log('action', '%s suspected %s', caller, ipstr)
|
|
end
|
|
return true, ('Suspected %s'):format(ipstr)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('ip_unsuspect', {
|
|
description='Unmark an IP as suspicious',
|
|
params='<IP> [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local ipint, ipstr, ip_status, reason = parse_ip_status_params(params)
|
|
if not ipint then
|
|
return false, reason
|
|
end
|
|
if ip_status.id ~= data.player_status.suspicious.id then
|
|
return false, ('IP %s is not suspicious!'):format(ipstr)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_ip_status(ipint, executor_id, data.ip_status.default.id, reason) then
|
|
return false, 'ERROR setting IP status'
|
|
end
|
|
if reason then
|
|
log('action', '%s unsuspected %s because %s', caller, ipstr, reason)
|
|
else
|
|
log('action', '%s unsuspected %s', caller, ipstr)
|
|
end
|
|
return true, ('Unsuspected %s'):format(ipstr)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('ip_block', {
|
|
description='Block an IP from connecting.',
|
|
params='<IP> [<reason>]',
|
|
privs={[admin_priv]=true},
|
|
func=function(caller, params)
|
|
local ipint, ipstr, ip_status, reason = parse_ip_status_params(params)
|
|
if not ipint then
|
|
return false, reason
|
|
end
|
|
local timespan
|
|
local expires
|
|
if reason then
|
|
local first = reason:match('^(%S+)')
|
|
timespan = parse_timespan(first)
|
|
if timespan then
|
|
reason = reason:sub(first:len() + 2)
|
|
expires = os.time() + timespan
|
|
end
|
|
end
|
|
if not table_contains({
|
|
data.ip_status.default.id,
|
|
data.ip_status.suspicious.id,
|
|
}, ip_status.id) then
|
|
return false, ('Cannot block IP w/ status %s'):format(ip_status.name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_ip_status(ipint, executor_id, data.ip_status.blocked.id, reason, expires) then
|
|
return false, 'ERROR setting IP status'
|
|
end
|
|
util.safe_kick_ip(ipstr)
|
|
if expires then
|
|
if reason then
|
|
log('action', '%s blocked %s until %s because %s', caller, ipstr, iso_date(expires), reason)
|
|
else
|
|
log('action', '%s blocked %s until %s', caller, ipstr, iso_date(expires))
|
|
end
|
|
else
|
|
if reason then
|
|
log('action', '%s blocked %s because %s', caller, ipstr, reason)
|
|
else
|
|
log('action', '%s blocked %s', caller, ipstr)
|
|
end
|
|
end
|
|
return true, ('Blocked %s'):format(ipstr)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('ip_unblock', {
|
|
description='Unblock an IP',
|
|
params='<IP> [<reason>]',
|
|
privs={[admin_priv]=true},
|
|
func=function(caller, params)
|
|
local ipint, ipstr, ip_status, reason = parse_ip_status_params(params)
|
|
if not ipint then
|
|
return false, reason
|
|
end
|
|
if ip_status.id ~= data.ip_status.blocked.id then
|
|
return false, 'IP is not blocked!'
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_ip_status(ipint, executor_id, data.ip_status.default.id, reason) then
|
|
return false, 'ERROR setting IP status'
|
|
end
|
|
if reason then
|
|
log('action', '%s unblocked %s because %s', caller, ipstr, reason)
|
|
else
|
|
log('action', '%s unblocked %s', caller, ipstr)
|
|
end
|
|
return true, ('Unblocked %s'):format(ipstr)
|
|
end
|
|
})
|
|
----------------- SET ASN STATUS COMMANDS -----------------
|
|
local function parse_asn_status_params(params)
|
|
local asnstr, reason = params:match('^A?(%d+)%s+(.*)$')
|
|
if not asnstr then
|
|
asnstr = params:match('^A?(%d+)$')
|
|
end
|
|
if not asnstr then
|
|
return nil, nil, nil, ('Invalid argument(s): %q'):format(params)
|
|
end
|
|
local asn = tonumber(asnstr)
|
|
local description = lib_asn.get_description(asn)
|
|
if description == lib_asn.invalid_asn_description then
|
|
return nil, nil, nil, ('Not a valid ASN: %q'):format(params)
|
|
end
|
|
data.register_asn(asn)
|
|
local asn_status = data.get_asn_status(asn, true)
|
|
return asn, description, asn_status, reason
|
|
end
|
|
|
|
register_chatcommand('asn_suspect', {
|
|
description='Mark an ASN as suspicious.',
|
|
params='<ASN> [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local asn, description, asn_status, reason = parse_asn_status_params(params)
|
|
if not asn then
|
|
return false, reason
|
|
end
|
|
if asn_status.id ~= data.asn_status.default.id then
|
|
return false, ('Cannot suspect ASN w/ status %s'):format(asn_status.name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_asn_status(asn, executor_id, data.asn_status.suspicious.id, reason) then
|
|
return false, 'ERROR setting ASN status'
|
|
end
|
|
if reason then
|
|
log('action', '%s suspected A%s because %s', caller, asn, reason)
|
|
else
|
|
log('action', '%s suspected A%s', caller, asn)
|
|
end
|
|
return true, ('Suspected A%s (%s)'):format(asn, description)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('asn_unsuspect', {
|
|
description='Unmark an ASN as suspicious.',
|
|
params='<ASN> [<reason>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local asn, description, asn_status, reason = parse_asn_status_params(params)
|
|
if not asn then
|
|
return false, reason
|
|
end
|
|
if asn_status.id ~= data.asn_status.suspicious.id then
|
|
return false, ('A%s is not suspicious!'):format(asn)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_asn_status(asn, executor_id, data.asn_status.default.id, reason) then
|
|
return false, 'ERROR setting ASN status'
|
|
end
|
|
if reason then
|
|
log('action', '%s unsuspected A%s because %s', caller, asn, reason)
|
|
else
|
|
log('action', '%s unsuspected A%s', caller, asn)
|
|
end
|
|
return true, ('Unsuspected A%s (%q)'):format(asn, description)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('asn_block', {
|
|
description='Block an ASN. Duration and reason optional.',
|
|
params='<ASN> [<duration>] [<reason>]',
|
|
privs={[admin_priv]=true},
|
|
func=function(caller, params)
|
|
local asn, description, asn_status, reason = parse_asn_status_params(params)
|
|
if not asn then
|
|
return false, reason
|
|
end
|
|
local timespan
|
|
local expires
|
|
if reason then
|
|
local first = reason:match('^(%S+)')
|
|
timespan = parse_timespan(first)
|
|
if timespan then
|
|
reason = reason:sub(first:len() + 2)
|
|
expires = os.time() + timespan
|
|
end
|
|
end
|
|
if not table_contains({
|
|
data.asn_status.default.id,
|
|
data.asn_status.suspicious.id,
|
|
}, asn_status.id) then
|
|
return false, ('Cannot block ASN w/ status %s'):format(asn_status.name)
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_asn_status(asn, executor_id, data.asn_status.blocked.id, reason, expires) then
|
|
return false, 'ERROR setting ASN status'
|
|
end
|
|
util.safe_kick_asn(asn)
|
|
if expires then
|
|
if reason then
|
|
log('action', '%s blocked A%s until %s because %s', caller, asn, iso_date(expires), reason)
|
|
else
|
|
log('action', '%s blocked A%s until %s ', caller, asn, iso_date(expires))
|
|
end
|
|
else
|
|
if reason then
|
|
log('action', '%s blocked A%s because %s', caller, asn, reason)
|
|
else
|
|
log('action', '%s blocked A%s', caller, asn)
|
|
end
|
|
end
|
|
return true, ('Blocked A%s (%q)'):format(asn, description)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('asn_unblock', {
|
|
description='Unblock an ASN.',
|
|
params='<ASN> [<reason>]',
|
|
privs={[admin_priv]=true},
|
|
func=function(caller, params)
|
|
local asn, description, asn_status, reason = parse_asn_status_params(params)
|
|
if not asn then
|
|
return false, reason
|
|
end
|
|
if asn_status.id ~= data.asn_status.blocked.id then
|
|
return false, 'ASN is not blocked!'
|
|
end
|
|
local executor_id = data.get_player_id(caller)
|
|
if not executor_id then
|
|
return false, 'ERROR: could not get executor ID?'
|
|
end
|
|
if not data.set_asn_status(asn, executor_id, data.asn_status.default.id, reason) then
|
|
return false, 'ERROR setting IP status'
|
|
end
|
|
if reason then
|
|
log('action', '%s unblocked A%s because %s', caller, asn, reason)
|
|
else
|
|
log('action', '%s unblocked A%s', caller, asn)
|
|
end
|
|
return true, ('Unblocked A%s (%q)'):format(asn, description)
|
|
end
|
|
})
|
|
---------------- GET LOGS ---------------
|
|
register_chatcommand('player_status', {
|
|
description='shows the status log of a player',
|
|
params='<player_name> [<number>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local name, numberstr = string.match(params, '^([%a%d_-]+)%s+(%d+)$')
|
|
if not name then
|
|
name = string.match(params, '^([%a%d_-]+)$')
|
|
end
|
|
if not name then
|
|
return false, 'invalid arguments'
|
|
end
|
|
local player_id, name = data.get_player_id(name)
|
|
if not player_id then
|
|
return false, 'unknown player'
|
|
end
|
|
local rows = data.get_player_status_log(player_id)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows == 0 then
|
|
return true, ('No records found for %s.'):format(name)
|
|
end
|
|
local starti
|
|
if numberstr then
|
|
local number = tonumber(numberstr)
|
|
starti = math.max(1, #rows - number)
|
|
else
|
|
starti = 1
|
|
end
|
|
chat_send_player(caller, ('Records for %s'):format(name))
|
|
for index = starti,#rows do
|
|
local row = rows[index]
|
|
local status_name = data.player_status_name[row.status_id] or data.player_status.default.name
|
|
local status_color = data.player_status_color[row.status_id] or data.player_status.default.color
|
|
local message = ('%s: %s set status to %s.'):format(
|
|
iso_date(row.timestamp),
|
|
row.executor_name,
|
|
minetest.colorize(status_color, status_name)
|
|
)
|
|
local reason = row.reason
|
|
if reason and reason ~= '' then
|
|
message = ('%s Reason: %s'):format(message, reason)
|
|
end
|
|
local expires = row.expires
|
|
if expires then
|
|
message = ('%s Expires: %s'):format(message, iso_date(expires))
|
|
end
|
|
chat_send_player(caller, message)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('ban_record', {
|
|
description = 'shows relevant info for a player',
|
|
params = '<player_name>',
|
|
privs = { [mod_priv] = true },
|
|
func = function(caller, params)
|
|
local player_name = params:match('^([a-zA-Z0-9_-]+)$')
|
|
if not player_name or player_name:len() > 20 then
|
|
return false, 'Invalid argument'
|
|
end
|
|
local player_id
|
|
player_id, player_name = data.get_player_id(player_name)
|
|
if not player_id then
|
|
return false, 'Unknown player'
|
|
end
|
|
local ipstr = data.fumble_about_for_an_ip(player_name, player_id)
|
|
local ipint = lib_ip.ipstr_to_ipint(ipstr)
|
|
local asn, asn_description = lib_asn.lookup(ipint)
|
|
|
|
chat_send_player(caller, "Ban record for %s", player_name)
|
|
|
|
local rows = data.get_player_cluster(player_id)
|
|
if not rows then return false, 'An error occurred (see server logs)' end
|
|
if #rows > 0 then
|
|
local clustered = {}
|
|
for _, row in ipairs(rows) do
|
|
local color = data.player_status_color[row.player_status_id] or data.player_status.default.color
|
|
table.insert(clustered, minetest.colorize(color, row.player_name))
|
|
end
|
|
chat_send_player(caller, "Accounts associated by IP:")
|
|
chat_send_player(caller, table.concat(clustered, ', '))
|
|
end
|
|
|
|
rows = data.get_asn_associations(asn, os.time() - parse_timespan('1y'))
|
|
if not rows then return false, 'An error occurred (see server logs)' end
|
|
if #rows > 0 then
|
|
local assocs = {}
|
|
for _, row in ipairs(rows) do
|
|
local color = data.player_status_color[row.player_status_id] or data.player_status.default.color
|
|
table.insert(assocs, minetest.colorize(color, row.player_name))
|
|
end
|
|
chat_send_player(caller, "Flagged accounts on A%s (%s):", asn, asn_description)
|
|
chat_send_player(caller, table.concat(assocs, ', '))
|
|
end
|
|
|
|
rows = data.get_player_status_log(player_id)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows > 0 then
|
|
chat_send_player(caller, "Account status:")
|
|
end
|
|
for index = 1,#rows do
|
|
local row = rows[index]
|
|
local status_name = data.player_status_name[row.status_id] or data.player_status.default.name
|
|
local status_color = data.player_status_color[row.status_id] or data.player_status.default.color
|
|
local message = ('%s: %s set status to %s.'):format(
|
|
iso_date(row.timestamp),
|
|
row.executor_name,
|
|
minetest.colorize(status_color, status_name)
|
|
)
|
|
local reason = row.reason
|
|
if reason and reason ~= '' then
|
|
message = ('%s Reason: %s'):format(message, reason)
|
|
end
|
|
local expires = row.expires
|
|
if expires then
|
|
message = ('%s Expires: %s'):format(message, iso_date(expires))
|
|
end
|
|
chat_send_player(caller, message)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('ip_status', {
|
|
description='shows the status log of an IP',
|
|
params='<IP> [<number>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local ipstr, numberstr = string.match(params, '^(%d+%.%d+%.%d+%.%d+)%s+(%d+)$')
|
|
if not ipstr then
|
|
ipstr = string.match(params, '^(%d+%.%d+%.%d+%.%d+)$')
|
|
end
|
|
if not ipstr or not lib_ip.is_valid_ip(ipstr) then
|
|
return false, 'invalid arguments'
|
|
end
|
|
local ipint = lib_ip.ipstr_to_ipint(ipstr)
|
|
local rows = data.get_ip_status_log(ipint)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows == 0 then
|
|
return true, 'No records found.'
|
|
end
|
|
local starti
|
|
if numberstr then
|
|
local number = tonumber(numberstr)
|
|
starti = math.max(1, #rows - number)
|
|
else
|
|
starti = 1
|
|
end
|
|
for index = starti,#rows do
|
|
local row = rows[index]
|
|
local status_name = data.ip_status_name[row.status_id] or data.ip_status.default.name
|
|
local status_color = data.ip_status_color[row.status_id] or data.ip_status.default.color
|
|
local message = ('%s: %s set status to %s.'):format(
|
|
iso_date(row.timestamp),
|
|
row.executor_name,
|
|
minetest.colorize(status_color, status_name)
|
|
)
|
|
local reason = row.reason
|
|
if reason and reason ~= '' then
|
|
message = ('%s Reason: %s'):format(message, reason)
|
|
end
|
|
local expires = row.expires
|
|
if expires then
|
|
message = ('%s Expires: %s'):format(message, iso_date(expires))
|
|
end
|
|
chat_send_player(caller, message)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('asn_status', {
|
|
description='shows the status log of an ASN',
|
|
params='<ASN> [<number>]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local asnstr, numberstr = string.match(params, '^A?(%d+)%s+(%d+)$')
|
|
if not asnstr then
|
|
asnstr = string.match(params, '^A?(%d+)$')
|
|
if not asnstr then
|
|
return false, 'invalid arguments'
|
|
end
|
|
end
|
|
local asn = tonumber(asnstr)
|
|
local rows = data.get_asn_status_log(asn)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows == 0 then
|
|
return true, 'No records found.'
|
|
end
|
|
local starti
|
|
if numberstr then
|
|
local number = tonumber(numberstr)
|
|
starti = math.max(1, #rows - number)
|
|
else
|
|
starti = 1
|
|
end
|
|
for index = starti,#rows do
|
|
local row = rows[index]
|
|
local status_name = data.asn_status_name[row.status_id] or data.asn_status.default.name
|
|
local status_color = data.asn_status_color[row.status_id] or data.asn_status.default.color
|
|
local message = ('%s: %s set status to %s.'):format(
|
|
iso_date(row.timestamp),
|
|
row.executor_name,
|
|
minetest.colorize(status_color, status_name)
|
|
)
|
|
local reason = row.reason
|
|
if reason and reason ~= '' then
|
|
message = ('%s Reason: %s'):format(message, reason)
|
|
end
|
|
local expires = row.expires
|
|
if expires then
|
|
message = ('%s Expires: %s'):format(message, iso_date(expires))
|
|
end
|
|
chat_send_player(caller, message)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('logins', {
|
|
description='shows the login record of a player',
|
|
params='<player_name> [<number>=20]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local name, limit = params:match('^([a-zA-Z0-9_-]+)%s+(%d+)$')
|
|
limit = tonumber(limit)
|
|
if not name then
|
|
name = params:match('^([a-zA-Z0-9_-]+)$')
|
|
limit = 20
|
|
end
|
|
if not name or not limit then
|
|
return false, 'Invalid arguments'
|
|
end
|
|
local player_id
|
|
player_id, name = data.get_player_id(name)
|
|
if not player_id then
|
|
return false, 'Unknown player'
|
|
end
|
|
local rows = data.get_player_connection_log(player_id, limit)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows == 0 then
|
|
return true, 'No records found.'
|
|
end
|
|
chat_send_player(caller, "Login record for %s", name)
|
|
for _, row in ipairs(rows) do
|
|
local ip_status_name = data.ip_status_name[row.ip_status_id] or data.ip_status.default.name
|
|
local ip_status_color = data.ip_status_color[row.ip_status_id] or data.ip_status.default.color
|
|
local asn_status_name = data.asn_status_name[row.asn_status_id] or data.asn_status.default.name
|
|
local asn_status_color = data.asn_status_color[row.asn_status_id] or data.asn_status.default.color
|
|
local asn_description = lib_asn.get_description(row.asn)
|
|
chat_send_player(
|
|
caller,
|
|
'%s:%s from %s<%s> A%s<%s> (%s)',
|
|
iso_date(row.timestamp),
|
|
(row.success and '') or ' failed!',
|
|
minetest.colorize(ip_status_color, lib_ip.ipint_to_ipstr(row.ipint)),
|
|
minetest.colorize(ip_status_color, ip_status_name),
|
|
minetest.colorize(asn_status_color, row.asn),
|
|
minetest.colorize(asn_status_color, asn_status_name),
|
|
minetest.colorize(asn_status_color, asn_description)
|
|
)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('inspect', {
|
|
description='List IPs and ASNs associated with a player',
|
|
params='<player_name>',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local name = params:match('^([a-zA-Z0-9_-]+)$')
|
|
if not name or name:len() > 20 then
|
|
return false, 'Invalid argument'
|
|
end
|
|
local player_id, name = data.get_player_id(name)
|
|
if not player_id then
|
|
return false, 'Unknown player'
|
|
end
|
|
local rows = data.get_player_associations(player_id)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows == 0 then
|
|
return true, 'No records found.'
|
|
end
|
|
chat_send_player(caller, 'Records for %s', name)
|
|
for _, row in ipairs(rows) do
|
|
local ipstr = lib_ip.ipint_to_ipstr(row.ipint)
|
|
local asn_description = lib_asn.get_description(row.asn)
|
|
local ip_status_name = data.ip_status_name[row.ip_status_id] or data.ip_status.default.name
|
|
local ip_status_color = data.ip_status_color[row.ip_status_id] or data.ip_status.default.color
|
|
local asn_status_name = data.asn_status_name[row.asn_status_id] or data.asn_status.default.name
|
|
local asn_status_color = data.asn_status_color[row.asn_status_id] or data.asn_status.default.color
|
|
chat_send_player(
|
|
caller,
|
|
'%s<%s> A%s (%s) <%s>',
|
|
minetest.colorize(ip_status_color, ipstr),
|
|
minetest.colorize(ip_status_color, ip_status_name),
|
|
minetest.colorize(asn_status_color, row.asn),
|
|
minetest.colorize(asn_status_color, asn_description),
|
|
minetest.colorize(asn_status_color, asn_status_name)
|
|
)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('ip_inspect', {
|
|
description='list player accounts and statuses associated with an IP',
|
|
params='<IP> [<timespan>=1m]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local ipstr, timespan_str = params:match('^(%d+%.%d+%.%d+%.%d+)%s+(%w+)$')
|
|
if not lib_ip.is_valid_ip(ipstr) then
|
|
ipstr = params:match('^%s*(%d+%.%d+%.%d+%.%d+)%s*$')
|
|
if not lib_ip.is_valid_ip(ipstr) then
|
|
return false, 'Invalid arguments'
|
|
end
|
|
end
|
|
local timespan
|
|
if timespan_str then
|
|
timespan = parse_timespan(timespan_str)
|
|
if not timespan then
|
|
return false, 'Invalid timespan'
|
|
end
|
|
else
|
|
timespan = parse_timespan('1m')
|
|
end
|
|
local ipint = lib_ip.ipstr_to_ipint(ipstr)
|
|
local rows = data.get_ip_associations(ipint, timespan)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows == 0 then
|
|
return true, 'No records found.'
|
|
end
|
|
chat_send_player(caller, ('Records for %s'):format(ipstr))
|
|
for _, row in ipairs(rows) do
|
|
local status_name = data.player_status_name[row.player_status_id] or data.player_status.default.name
|
|
local status_color = data.player_status_color[row.player_status_id] or data.player_status.default.color
|
|
local message = ('% 20s: %s'):format(
|
|
row.player_name,
|
|
minetest.colorize(status_color, status_name)
|
|
)
|
|
chat_send_player(caller, message)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('asn_inspect', {
|
|
description='list *FLAGGED* player accounts and statuses associated with an ASN',
|
|
params='<ASN> [<timespan>=1y]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local asn, timespan_str = params:match('^A?(%d+)%s+(%w+)$')
|
|
if not asn then
|
|
asn = params:match('^A?(%d+)$')
|
|
if not asn then
|
|
return false, 'Invalid argument'
|
|
end
|
|
end
|
|
local timespan
|
|
if timespan_str then
|
|
timespan = parse_timespan(timespan_str)
|
|
if not timespan then
|
|
return false, 'Invalid timespan'
|
|
end
|
|
else
|
|
timespan = parse_timespan('1y')
|
|
end
|
|
asn = tonumber(asn)
|
|
local description = lib_asn.get_description(asn)
|
|
local start_time = os.time() - timespan
|
|
local rows = data.get_asn_associations(asn, start_time)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows == 0 then
|
|
return true, 'No records found.'
|
|
end
|
|
chat_send_player(caller, ('Records for A%s : %s'):format(asn, description))
|
|
for _, row in ipairs(rows) do
|
|
local status_name = data.player_status_name[row.player_status_id] or data.player_status.default.name
|
|
local status_color = data.player_status_color[row.player_status_id] or data.player_status.default.color
|
|
local message = ('% 20s: %s (last IP: %s)'):format(
|
|
row.player_name,
|
|
minetest.colorize(status_color, status_name),
|
|
lib_ip.ipint_to_ipstr(row.ipint)
|
|
)
|
|
chat_send_player(caller, message)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('asn_stats', {
|
|
description='Get statistics for an ASN',
|
|
params='<ASN>',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local asnstr = params:match('^A?(%d+)$')
|
|
if not asnstr then
|
|
return false, 'Invalid argument'
|
|
end
|
|
local asn = tonumber(asnstr)
|
|
local asn_description = lib_asn.get_description(asn)
|
|
local rows = data.get_asn_stats(asn)
|
|
if not rows then
|
|
return false, 'Error: see server log'
|
|
elseif #rows == 0 then
|
|
return true, 'No data'
|
|
end
|
|
chat_send_player(caller, ('Statistics for %s:'):format(asn_description))
|
|
for _, row in ipairs(rows) do
|
|
local status_name = data.player_status_name[row.player_status_id] or data.player_status.default.name
|
|
local status_color = data.player_status_color[row.player_status_id] or data.player_status.default.color
|
|
chat_send_player(caller, ('%s %s'):format(
|
|
minetest.colorize(status_color, status_name),
|
|
row.count
|
|
))
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('cluster', {
|
|
description='Get a list of other players who have ever shared an IP w/ the given player',
|
|
params='<player_name>',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local player_name = params:match('^([a-zA-Z0-9_-]+)$')
|
|
if not player_name or player_name:len() > 20 then
|
|
return false, 'Invalid argument'
|
|
end
|
|
local player_id
|
|
player_id, player_name = data.get_player_id(player_name)
|
|
if not player_id then
|
|
return false, 'Unknown player'
|
|
end
|
|
local rows = data.get_player_cluster(player_id)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows == 0 then
|
|
return true, 'No records found.'
|
|
end
|
|
chat_send_player(caller, "Accounts associated with %s:", player_name)
|
|
for _, row in ipairs(rows) do
|
|
local status_name = data.player_status_name[row.player_status_id] or data.player_status.default.name
|
|
local status_color = data.player_status_color[row.player_status_id] or data.player_status.default.color
|
|
chat_send_player(
|
|
caller,
|
|
'% 20s: %s',
|
|
row.player_name,
|
|
minetest.colorize(status_color, status_name)
|
|
)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
alias_chatcommand('/whois', 'cluster')
|
|
--
|
|
register_chatcommand('who2', {
|
|
description='Show current connected players, statuses, and sources',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller)
|
|
local names = {}
|
|
for _, player in ipairs(minetest.get_connected_players()) do
|
|
table.insert(names, player:get_player_name())
|
|
end
|
|
table.sort(names, function(a, b) return a:lower() < b:lower() end)
|
|
for _, name in ipairs(names) do
|
|
local player_id = data.get_player_id(name)
|
|
local player_status = data.get_player_status(player_id)
|
|
local player_status_color = data.player_status_color[player_status.id]
|
|
|
|
local ipstr = data.fumble_about_for_an_ip(name, player_id)
|
|
local ipint = lib_ip.ipstr_to_ipint(ipstr)
|
|
local ip_status = data.get_ip_status(ipint) or data.ip_status.default
|
|
local ip_status_color = data.ip_status_color[ip_status.id]
|
|
|
|
local asn, asn_description = lib_asn.lookup(ipint)
|
|
local asn_status = data.get_asn_status(asn)
|
|
local asn_status_color = data.asn_status_color[asn_status.id]
|
|
|
|
local message = ('% 20s<%s> %s<%s> A%s<%s> (%s)'):format(
|
|
minetest.colorize(player_status_color, name),
|
|
minetest.colorize(player_status_color, player_status.name),
|
|
minetest.colorize(ip_status_color, ipstr),
|
|
minetest.colorize(ip_status_color, ip_status.name),
|
|
minetest.colorize(asn_status_color, asn),
|
|
minetest.colorize(asn_status_color, asn_status.name),
|
|
minetest.colorize(asn_status_color, asn_description)
|
|
)
|
|
chat_send_player(caller, message)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('bans', {
|
|
description='Get a list of recent player status changes',
|
|
params='[<number>=20]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local limit
|
|
if params and params ~= '' then
|
|
limit = params:match('^%s*(%d+)%s*$')
|
|
if limit then
|
|
limit = tonumber(limit)
|
|
else
|
|
return false, 'Invalid argument'
|
|
end
|
|
else
|
|
limit = 20
|
|
end
|
|
local rows = data.get_ban_log(limit)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
end
|
|
if #rows == 0 then
|
|
return true, 'No records found.'
|
|
end
|
|
for _, row in ipairs(rows) do
|
|
local status_name = data.player_status_name[row.status_id] or data.player_status.default.name
|
|
local status_color = data.player_status_color[row.status_id] or data.player_status.default.color
|
|
local message = ('%s: %s %s %s'):format(
|
|
iso_date(row.timestamp),
|
|
row.executor_name,
|
|
minetest.colorize(status_color, status_name),
|
|
row.player_name
|
|
)
|
|
if row.expires then
|
|
message = message .. (' until %s'):format(iso_date(row.expires))
|
|
end
|
|
if row.reason then
|
|
message = message .. (' because %s'):format(row.reason)
|
|
end
|
|
chat_send_player(caller, message)
|
|
end
|
|
end
|
|
})
|
|
|
|
--------------
|
|
|
|
-- prevent players from flooding reports
|
|
local report_times_by_player = {}
|
|
|
|
register_chatcommand('report', {
|
|
description='Send a report to server staff',
|
|
params='<report>',
|
|
func=function(reporter, report)
|
|
-- TODO make the hard-coded values here settings
|
|
local now = os.time()
|
|
local report_times = report_times_by_player[reporter]
|
|
if not report_times then
|
|
report_times_by_player[reporter] = {now}
|
|
else
|
|
while #report_times > 0 and (now - report_times[1]) > 3600 do
|
|
table.remove(report_times, 1)
|
|
end
|
|
if #report_times >= 5 then
|
|
return false, 'You may only issue 5 reports in an hour.'
|
|
end
|
|
table.insert(report_times, now)
|
|
end
|
|
if report == '' then
|
|
return false, 'You must enter a report!'
|
|
end
|
|
local reporter_id = data.get_player_id(reporter)
|
|
if not data.add_report(reporter_id, report) then
|
|
return false, 'Error: check server log'
|
|
end
|
|
return true, 'Report sent.'
|
|
end
|
|
})
|
|
|
|
register_chatcommand('reports', {
|
|
description='View recent reports',
|
|
params='[<timespan>=1w]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, timespan_str)
|
|
local timespan
|
|
if timespan_str ~= '' then
|
|
timespan = parse_timespan(timespan_str)
|
|
if not timespan then
|
|
return false, 'Invalid timespan'
|
|
end
|
|
else
|
|
timespan = 60*60*24*7
|
|
end
|
|
local from_time = os.time() - timespan
|
|
local rows = data.get_reports(from_time)
|
|
if not rows then
|
|
return false, 'An error occurred (see server logs)'
|
|
elseif #rows == 0 then
|
|
return true, 'No records found.'
|
|
end
|
|
for _, row in ipairs(rows) do
|
|
local message = ('%s % 20s: %s'):format(
|
|
iso_date(row.timestamp),
|
|
row.reporter,
|
|
row.report
|
|
)
|
|
chat_send_player(caller, message)
|
|
end
|
|
end
|
|
})
|
|
|
|
register_chatcommand('first-login', {
|
|
description='Get the first login time of any player or yourself.',
|
|
params='[<player_name>]',
|
|
func=function(reporter, params)
|
|
if params == '' then
|
|
params = reporter
|
|
end
|
|
local player_id, player_name = data.get_player_id(params)
|
|
if not player_id then
|
|
return false, 'Unknown player'
|
|
end
|
|
local rows = data.get_first_login(player_id)
|
|
if not rows then
|
|
return false, 'An error occurred. See server logs.'
|
|
elseif #rows == 0 then
|
|
return true, 'No record of player logging in'
|
|
end
|
|
return true, ("%s: %s"):format(player_name, iso_date(rows[1].timestamp))
|
|
end
|
|
})
|
|
|
|
register_chatcommand('master', {
|
|
description='Set the master account of an alt account',
|
|
params='<alt> <master>',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local alt_name, master_name = params:match('^%s*([a-zA-Z0-9_-]+)%s+([a-zA-Z0-9_-]+)%s*$')
|
|
if not alt_name or not master_name or alt_name:len() > 20 or master_name:len() > 20 then
|
|
return false, 'Invalid arguments'
|
|
end
|
|
local alt_id, true_alt_name = data.get_player_id(alt_name)
|
|
local master_id = data.get_player_id(master_name)
|
|
if not alt_id then
|
|
return false, ('Unknown player %s'):format(alt_name)
|
|
elseif not master_id then
|
|
return false, ('Unknown player %s'):format(master_name)
|
|
end
|
|
local set_status, message = data.set_master(alt_id, master_id)
|
|
if not set_status then
|
|
return false, ('An error occured (%s). Check logs.'):format(message)
|
|
end
|
|
local true_master_id, true_master_name = data.get_master(alt_id)
|
|
local status = data.get_player_status(true_master_id)
|
|
if status.id == data.player_status.banned.id then
|
|
local alts = data.get_alts(true_master_id)
|
|
for _, other_alt_name in ipairs(alts) do
|
|
local player = minetest.get_player_by_name(other_alt_name)
|
|
if player then
|
|
util.safe_kick_player(caller, player, status.reason)
|
|
end
|
|
end
|
|
end
|
|
return true, ('Set master of %s to %s'):format(true_alt_name, true_master_name)
|
|
end
|
|
})
|
|
|
|
register_chatcommand('unmaster', {
|
|
description='Remove the master from an alt account',
|
|
params='<player_name>',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local alt_name = params:match('^%s*([a-zA-Z0-9_-]+)%s*$')
|
|
if not alt_name or alt_name:len() > 20 then
|
|
return false, 'Invalid argument'
|
|
end
|
|
local alt_id = data.get_player_id(alt_name)
|
|
if not alt_id then
|
|
return false, 'Unknown player'
|
|
end
|
|
local master_id = data.get_master_id(alt_id)
|
|
if not master_id then
|
|
return false, 'Player has no master ID'
|
|
end
|
|
if not data.unset_master(alt_id) then
|
|
return false, 'Error (see logs)'
|
|
end
|
|
return true, 'Master removed'
|
|
end
|
|
})
|
|
|
|
register_chatcommand('alts', {
|
|
description = 'List alts of a player',
|
|
params = '<player_name>',
|
|
privs = { [mod_priv] = true },
|
|
func = function(caller, player_name)
|
|
local alt_id, player_name = data.get_player_id(player_name)
|
|
if not alt_id then
|
|
return false, 'Unknown or invalid player name'
|
|
end
|
|
local master_id, master_name = data.get_master(alt_id)
|
|
if not master_id then
|
|
master_id = alt_id
|
|
master_name = player_name
|
|
end
|
|
local alts = data.get_alts(master_id)
|
|
if not alts then
|
|
return false, 'An error occured (see server logs)'
|
|
end
|
|
if #alts == 0 then
|
|
return true, ('%s has no alts'):format(master_name)
|
|
else
|
|
chat_send_player(caller, 'Master account: %s', master_name)
|
|
chat_send_player(caller, 'Alts: %s', table.concat(alts, ', '))
|
|
end
|
|
end
|
|
})
|
|
|
|
register_chatcommand('pgrep', {
|
|
description='Search for players by name, using a SQLite GLOB expression (e.g. "*foo*")',
|
|
params='<pattern> [<limit>=20]',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, params)
|
|
local pattern, limit = params:match('^%s*(%S+)%s+(%d+)%s*$')
|
|
if not limit or not pattern then
|
|
pattern = params:match('^%s*(%S+)%s*$')
|
|
if not pattern then
|
|
return false, 'Invalid arguments'
|
|
end
|
|
limit = 20
|
|
end
|
|
local rows = data.grep_player(pattern, limit)
|
|
if not rows then
|
|
return false, 'Error (probably a malformed regular expression)'
|
|
elseif #rows == 0 then
|
|
return true, 'No matches'
|
|
end
|
|
for _, row in ipairs(rows) do
|
|
local status_name = data.player_status_name[row.player_status_id] or data.player_status.default.name
|
|
local status_color = data.player_status_color[row.player_status_id] or data.player_status.default.color
|
|
local ipstr = (row.ipint and lib_ip.ipint_to_ipstr(row.ipint)) or ''
|
|
local asn = row.asn or ''
|
|
local asn_description = (row.asn and lib_asn.get_description(row.asn)) or ''
|
|
chat_send_player(caller, '%s %s %s %s (%s)',
|
|
row.name,
|
|
minetest.colorize(status_color, status_name),
|
|
ipstr,
|
|
asn,
|
|
asn_description
|
|
)
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
|
|
register_chatcommand('unflag', {
|
|
description='remove a flag from a player',
|
|
params='<player_name>',
|
|
privs={[mod_priv]=true},
|
|
func=function(caller, player_name)
|
|
local player_id, player_name = data.get_player_id(player_name)
|
|
if not player_id then
|
|
return false, 'Unknown player'
|
|
end
|
|
if data.flag_player(player_id, false) then
|
|
return true, ('Unflagged %s'):format(player_name)
|
|
else
|
|
return false, 'Error (see server log)'
|
|
end
|
|
end
|
|
})
|
|
|