checkpoint
parent
4e383f59f6
commit
e85eea5539
208
commands.lua
208
commands.lua
|
@ -49,21 +49,104 @@ minetest.register_chatcommand('get_asn', {
|
|||
end
|
||||
})
|
||||
|
||||
local function parse_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 = verbana.data.get_player_id(name)
|
||||
if not player_id then
|
||||
return nil, nil, nil, ('Unknown player: %s'):format(name)
|
||||
end
|
||||
local player_status = verbana.data.get_player_status(player_id, true)
|
||||
return player_id, name, player_status, reason
|
||||
end
|
||||
|
||||
local function has_suspicious_connection(player_name)
|
||||
local connection_log = verbana.data.get_player_connection_log(player_name, 1)
|
||||
if not connection_log or #connection_log ~= 1 then
|
||||
verbana.log('warning', 'player %s exists but has no connection log?', player_name)
|
||||
return true
|
||||
end
|
||||
local last_login = connection_log[1]
|
||||
if last_login.ip_status_id == verbana.data.ip_status.trusted.id then
|
||||
return false
|
||||
elseif last_login.ip_status_id ~= verbana.data.ip_status.default.id then
|
||||
return true
|
||||
elseif last_login.asn_status_id == verbana.data.asn_status.default.id then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
minetest.register_chatcommand('verify', {
|
||||
params='<name>',
|
||||
params='<name> [<reason>]',
|
||||
description='verify a player',
|
||||
privs={[mod_priv]=true},
|
||||
func=function(caller, params)
|
||||
return false, 'TODO: implement' -- TODO
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
if player_status.status_id ~= verbana.data.player_status.unverified.id then
|
||||
return false, ('Player %s is not unverified'):format(player_name)
|
||||
end
|
||||
local executor_id = verbana.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_name) then
|
||||
status_id = verbana.data.player_status.suspicious.id
|
||||
else
|
||||
status_id = verbana.data.player_status.default.id
|
||||
end
|
||||
if not verbana.data.set_player_status(player_id, executor_id, status_id, reason) then
|
||||
return false, 'ERROR setting player status'
|
||||
end
|
||||
minetest.set_player_privs(player_name, verbana.settings.verified_privs)
|
||||
local player = minetest.get_player_by_name(player_name)
|
||||
if player then
|
||||
player:set_pos(verbana.settings.spawn_pos)
|
||||
else
|
||||
-- TODO: set up some way to TP the player to spawn when they log in
|
||||
end
|
||||
return true, ('Verified %s'):format(player_name)
|
||||
end
|
||||
})
|
||||
|
||||
minetest.register_chatcommand('unverify', {
|
||||
params='<name>',
|
||||
params='<name> [<reason>]',
|
||||
description='unverify a player',
|
||||
privs={[mod_priv]=true},
|
||||
func=function(caller, params)
|
||||
return false, 'TODO: implement' -- TODO
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
if not verbana.util.table_contains({
|
||||
verbana.data.player_status.unknown,
|
||||
verbana.data.player_status.default,
|
||||
verbana.data.player_status.suspicious,
|
||||
}, player_status.status_id) then
|
||||
return false, ('Cannot unverify %s w/ status %s'):format(player_name, verbana.data.player_status[player_status.status_id].name)
|
||||
end
|
||||
local executor_id = verbana.data.get_player_id(caller)
|
||||
if not executor_id then
|
||||
return false, 'ERROR: could not get executor ID?'
|
||||
end
|
||||
if not verbana.data.set_player_status(player_id, executor_id, verbana.data.player_status.unverified.id, reason) then
|
||||
return false, 'ERROR setting player status'
|
||||
end
|
||||
minetest.set_player_privs(player_name, verbana.settings.unverified_privs)
|
||||
local player = minetest.get_player_by_name(player_name)
|
||||
if player then
|
||||
player:set_pos(verbana.settings.verification_pos)
|
||||
end
|
||||
return true, ('Unverified %s'):format(player_name)
|
||||
end
|
||||
})
|
||||
|
||||
|
@ -72,6 +155,10 @@ minetest.override_chatcommand('kick', {
|
|||
description='kick a player',
|
||||
privs={[mod_priv]=true},
|
||||
func=function(caller, params)
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
return false, 'TODO: implement' -- TODO
|
||||
end
|
||||
})
|
||||
|
@ -81,6 +168,10 @@ minetest.register_chatcommand('lock', {
|
|||
description='lock a player\'s account',
|
||||
privs={[mod_priv]=true},
|
||||
func=function(caller, params)
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
return false, 'TODO: implement' -- TODO
|
||||
end
|
||||
})
|
||||
|
@ -90,6 +181,10 @@ minetest.register_chatcommand('unlock', {
|
|||
description='unlock a player\'s account',
|
||||
privs={[mod_priv]=true},
|
||||
func=function(caller, params)
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
return false, 'TODO: implement' -- TODO
|
||||
end
|
||||
})
|
||||
|
@ -99,6 +194,10 @@ minetest.override_chatcommand('ban', {
|
|||
description='ban a player',
|
||||
privs={[mod_priv]=true},
|
||||
func=function(caller, params)
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
-- todo: make sure that the begining of 'reason' doesn't look like a timespan =b
|
||||
return false, 'TODO: implement' -- TODO
|
||||
end
|
||||
|
@ -118,6 +217,10 @@ minetest.override_chatcommand('unban', {
|
|||
description='unban a player',
|
||||
privs={[mod_priv]=true},
|
||||
func=function(caller, params)
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
return false, 'TODO: implement' -- TODO
|
||||
end
|
||||
})
|
||||
|
@ -127,6 +230,10 @@ minetest.register_chatcommand('whitelist', {
|
|||
description='whitelist a player',
|
||||
privs={[admin_priv]=true},
|
||||
func=function(caller, params)
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
return false, 'TODO: implement' -- TODO
|
||||
end
|
||||
})
|
||||
|
@ -136,6 +243,10 @@ minetest.register_chatcommand('unwhitelist', {
|
|||
description='whitelist a player',
|
||||
privs={[admin_priv]=true},
|
||||
func=function(caller, params)
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
return false, 'TODO: implement' -- TODO
|
||||
end
|
||||
})
|
||||
|
@ -145,6 +256,10 @@ minetest.register_chatcommand('suspect', {
|
|||
description='mark a player as suspicious',
|
||||
privs={[mod_priv]=true},
|
||||
func=function(caller, params)
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
return false, 'TODO: implement' -- TODO
|
||||
end
|
||||
})
|
||||
|
@ -154,6 +269,10 @@ minetest.register_chatcommand('unsuspect', {
|
|||
description='unmark a player as suspicious',
|
||||
privs={[mod_priv]=true},
|
||||
func=function(caller, params)
|
||||
local player_id, player_name, player_status, reason = parse_status_params(params)
|
||||
if not player_id then
|
||||
return false, reason
|
||||
end
|
||||
return false, 'TODO: implement' -- TODO
|
||||
end
|
||||
})
|
||||
|
@ -170,46 +289,47 @@ minetest.register_chatcommand('ban_record', {
|
|||
return false, 'invalid arguments'
|
||||
end
|
||||
end
|
||||
|
||||
local rows = verbana.data.get_ban_record(name)
|
||||
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 executor = row[1]
|
||||
local status = row[2]
|
||||
local timestamp = os.date("%c", row[3])
|
||||
local reason = row[4]
|
||||
local expires
|
||||
if row[5] then
|
||||
expires = os.date("%c", row[5])
|
||||
end
|
||||
local message = ('%s: %s set status to %s.'):format(timestamp, executor, status)
|
||||
if reason and reason ~= '' then
|
||||
message = ('%s Reason: %s'):format(message, reason)
|
||||
end
|
||||
if expires then
|
||||
message = ('%s Expires: %s'):format(message, expires)
|
||||
end
|
||||
|
||||
minetest.chat_send_player(caller, message)
|
||||
end
|
||||
|
||||
return true
|
||||
-- TODO: there is no more "ban record" command, just "player status log"
|
||||
--
|
||||
-- local rows = verbana.data.get_ban_record(name)
|
||||
-- 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 executor = row[1]
|
||||
-- local status = row[2]
|
||||
-- local timestamp = os.date("%c", row[3])
|
||||
-- local reason = row[4]
|
||||
-- local expires
|
||||
-- if row[5] then
|
||||
-- expires = os.date("%c", row[5])
|
||||
-- end
|
||||
-- local message = ('%s: %s set status to %s.'):format(timestamp, executor, status)
|
||||
-- if reason and reason ~= '' then
|
||||
-- message = ('%s Reason: %s'):format(message, reason)
|
||||
-- end
|
||||
-- if expires then
|
||||
-- message = ('%s Expires: %s'):format(message, expires)
|
||||
-- end
|
||||
--
|
||||
-- minetest.chat_send_player(caller, message)
|
||||
-- end
|
||||
--
|
||||
-- return true
|
||||
end
|
||||
})
|
||||
|
||||
|
|
487
data.lua
487
data.lua
|
@ -6,40 +6,36 @@ if not verbana.log then function verbana.log(_, message, ...) print(message:form
|
|||
local sql = verbana.sql
|
||||
local db = verbana.db
|
||||
|
||||
local load_file = verbana.util.load_file
|
||||
|
||||
verbana.data = {}
|
||||
verbana.data.player_status_id = {
|
||||
unknown=1,
|
||||
default=2,
|
||||
unverified=3,
|
||||
banned=4,
|
||||
tempbanned=5,
|
||||
locked=6,
|
||||
whitelisted=7,
|
||||
suspicious=8,
|
||||
|
||||
verbana.data.player_status = {
|
||||
unknown={name='unknown', id=1},
|
||||
default={name='default', id=2},
|
||||
unverified={name='unverified', id=3},
|
||||
banned={name='banned', id=4},
|
||||
tempbanned={name='tempbanned', id=5},
|
||||
locked={name='locked', id=6},
|
||||
whitelisted={name='whitelisted', id=7},
|
||||
suspicious={name='suspicious', id=8},
|
||||
}
|
||||
verbana.data.player_status = verbana.util.table_invert(verbana.data.player_status_id)
|
||||
verbana.data.ip_status_id = {
|
||||
default=1,
|
||||
trusted=2,
|
||||
suspicious=3,
|
||||
blocked=4,
|
||||
tempblocked=5,
|
||||
verbana.data.ip_status = {
|
||||
default={name='default', id=1},
|
||||
trusted={name='trusted', id=2},
|
||||
suspicious={name='suspicious', id=3},
|
||||
blocked={name='blocked', id=4},
|
||||
tempblocked={name='tempblocked', id=5},
|
||||
}
|
||||
verbana.data.ip_status = verbana.util.table_invert(verbana.data.ip_status_id)
|
||||
verbana.data.asn_status_id = {
|
||||
default=1,
|
||||
suspicious=2,
|
||||
blocked=3,
|
||||
tempblocked=4,
|
||||
verbana.data.asn_status = {
|
||||
default={name='default', id=1},
|
||||
suspicious={name='suspicious', id=2},
|
||||
blocked={name='blocked', id=3},
|
||||
tempblocked={name='tempblocked', id=4},
|
||||
}
|
||||
verbana.data.asn_status = verbana.util.table_invert(verbana.data.asn_status_id)
|
||||
verbana.data.verbana_player = '!verbana!'
|
||||
verbana.data.verbana_player_id = 1
|
||||
|
||||
local function init_db()
|
||||
local schema = load_file(verbana.modpath .. '/schema.sql')
|
||||
local schema = verbana.util.load_file(verbana.modpath .. '/schema.sql')
|
||||
if not schema then
|
||||
error(('Could not find Verbana schema at %q'):format(verbana.modpath .. '/schema.sql'))
|
||||
end
|
||||
|
@ -76,7 +72,7 @@ local function bind(statement, description, ...)
|
|||
end
|
||||
|
||||
local function bind_and_step(statement, description, ...)
|
||||
if not bind(...) then return false end
|
||||
if not bind(statement, description, ...) then return false end
|
||||
if statement:step() ~= sql.DONE then
|
||||
verbana.log('unbans: stepping %s: %s', description, db:errmsg())
|
||||
return false
|
||||
|
@ -94,7 +90,7 @@ local function finalize(statement, description)
|
|||
end
|
||||
|
||||
local function execute_bind_one(code, description, ...)
|
||||
local statement = prepare(code)
|
||||
local statement = prepare(code, description)
|
||||
if not statement then return false end
|
||||
if not bind_and_step(statement, description, ...) then return false end
|
||||
if not finalize(statement, description) then return false end
|
||||
|
@ -104,7 +100,7 @@ end
|
|||
local function get_full_table(code, description, ...)
|
||||
local statement = prepare(code, description)
|
||||
if not statement then return nil end
|
||||
if not bind(statement, ...) then return nil end
|
||||
if not bind(statement, description, ...) then return nil end
|
||||
local rows = {}
|
||||
for row in statement:rows() do
|
||||
table.insert(rows, row)
|
||||
|
@ -113,9 +109,21 @@ local function get_full_table(code, description, ...)
|
|||
return rows
|
||||
end
|
||||
|
||||
local function get_full_ntable(code, description, ...)
|
||||
local statement = prepare(code, description)
|
||||
if not statement then return nil end
|
||||
if not bind(statement, description, ...) then return nil end
|
||||
local rows = {}
|
||||
for row in statement:nrows() do
|
||||
table.insert(rows, row)
|
||||
end
|
||||
if not finalize(statement, description) then return nil end
|
||||
return rows
|
||||
end
|
||||
|
||||
function verbana.data.import_from_sban(filename)
|
||||
-- apologies for the very long method
|
||||
local start = os.clock()
|
||||
-- this method isn't as complicated as it looks; 90% of it is repetative error handling
|
||||
if not execute('BEGIN TRANSACTION', 'sban import transaction') then
|
||||
return false
|
||||
end
|
||||
|
@ -151,14 +159,14 @@ function verbana.data.import_from_sban(filename)
|
|||
for id, name in db:urows('SELECT id, name FROM player') do
|
||||
player_id_by_name[name] = id
|
||||
end
|
||||
-- associations --
|
||||
-- ips, asns, associations, and logs --
|
||||
local insert_ip_statement = prepare('INSERT OR IGNORE INTO ip (ip) VALUES (?)', 'insert IP')
|
||||
if not insert_ip_statement then return _error() end
|
||||
local insert_asn_statement = prepare('INSERT OR IGNORE INTO asn (asn) VALUES (?)', 'insert ASN')
|
||||
if not insert_asn_statement then return _error() end
|
||||
local insert_assoc_statement = prepare('INSERT OR IGNORE INTO assoc (player_id, ip, asn) VALUES (?, ?, ?)', 'insert assoc')
|
||||
if not insert_assoc_statement then return _error() end
|
||||
local insert_log_statement = prepare('INSERT OR IGNORE INTO log (player_id, ip, asn, success, timestamp) VALUES (?, ?, ?, ?, ?)', 'insert log')
|
||||
local insert_log_statement = prepare('INSERT OR IGNORE INTO connection_log (player_id, ip, asn, success, timestamp) VALUES (?, ?, ?, ?, ?)', 'insert connection log')
|
||||
if not insert_log_statement then return _error() end
|
||||
for name, ipstr, created, last_login in sban_db:urows('SELECT name, ip, created, last_login FROM playerdata') do
|
||||
local player_id = player_id_by_name[name]
|
||||
|
@ -170,16 +178,16 @@ function verbana.data.import_from_sban(filename)
|
|||
if not bind_and_step(insert_ip_statement, 'insert IP', ipint) then return _error() end
|
||||
if not bind_and_step(insert_asn_statement, 'insert ASN', asn) then return _error() end
|
||||
if not bind_and_step(insert_assoc_statement, 'insert assoc', player_id, ipint, asn) then return _error() end
|
||||
if not bind_and_step(insert_log_statement, 'insert log', player_id, ipint, asn, true, created) then return _error() end
|
||||
if not bind_and_step(insert_log_statement, 'insert connection log', player_id, ipint, asn, true, created) then return _error() end
|
||||
end
|
||||
if not finalize(insert_ip_statement, 'insert IP') then return _error() end
|
||||
if not finalize(insert_asn_statement, 'insert ASN') then return _error() end
|
||||
if not finalize(insert_assoc_statement, 'insert assoc') then return _error() end
|
||||
if not finalize(insert_log_statement, 'insert log') then return _error() end
|
||||
if not finalize(insert_log_statement, 'insert connection log') then return _error() end
|
||||
-- player status --
|
||||
local default_player_status_id = verbana.data.player_status_id.default
|
||||
local banned_player_status_id = verbana.data.player_status_id.banned
|
||||
local tempbanned_player_status_id = verbana.data.player_status_id.tempbanned
|
||||
local default_player_status_id = verbana.data.player_status.default.id
|
||||
local banned_player_status_id = verbana.data.player_status.banned.id
|
||||
local tempbanned_player_status_id = verbana.data.player_status.tempbanned.id
|
||||
local insert_player_status_sql = [[
|
||||
INSERT OR IGNORE
|
||||
INTO player_status_log (executor_id, player_id, status_id, timestamp, reason, expires)
|
||||
|
@ -217,13 +225,13 @@ function verbana.data.import_from_sban(filename)
|
|||
end
|
||||
if not finalize(insert_player_status_statement, 'insert player status') then return _error() end
|
||||
-- SET LAST ACTION --
|
||||
local set_last_status_id_sql = [[
|
||||
local set_current_status_id_sql = [[
|
||||
UPDATE player
|
||||
SET last_status_id = (SELECT MAX(player_status_log.id)
|
||||
SET current_status_id = (SELECT MAX(player_status_log.id)
|
||||
FROM player_status_log
|
||||
WHERE player_status_log.player_id == player.id);
|
||||
]]
|
||||
if not execute(set_last_status_id_sql, 'set last status') then return _error() end
|
||||
if not execute(set_current_status_id_sql, 'set last status') then return _error() end
|
||||
-- CLEANUP --
|
||||
if not execute('COMMIT') then
|
||||
if sban_db:close() ~= sql.OK then
|
||||
|
@ -244,41 +252,376 @@ function verbana.data.get_player_id(name, create_if_new)
|
|||
if create_if_new then
|
||||
if not execute_bind_one('INSERT OR IGNORE INTO player (name) VALUES (?)', 'insert player', name) then return nil end
|
||||
end
|
||||
local table = get_full_table('SELECT id FROM player WHERE name = ? LIMIT 1', 'get player id')
|
||||
local table = get_full_table('SELECT id FROM player WHERE name = ? LIMIT 1', 'get player id', name)
|
||||
if not (table and table[1]) then return nil end
|
||||
return table[1][1]
|
||||
end
|
||||
function verbana.data.get_player_status(player_id) return {} end
|
||||
function verbana.data.set_player_status(player_id, executor_id, status_id, reason, expires) end
|
||||
|
||||
function verbana.data.get_ip_status(ipint) return {} end
|
||||
function verbana.data.set_ip_status(ipint, executor_id, status_name, reason, expires) end
|
||||
|
||||
function verbana.data.get_asn_status(asn) return {} end
|
||||
function verbana.data.set_asn_status(asn, executor_id, status_name, reason, expires) end
|
||||
|
||||
function verbana.data.log(player_id, ipint, asn, success) end
|
||||
|
||||
function verbana.data.assoc(player_id, ipint, asn) end
|
||||
function verbana.data.has_asn_assoc(player_id, asn) end
|
||||
function verbana.data.has_ip_assoc(player_id, ipint) end
|
||||
|
||||
function verbana.data.get_ban_record(player_name)
|
||||
local ban_record_sql = [[
|
||||
SELECT executor.name
|
||||
, player_status.name
|
||||
, player_status_log.timestamp
|
||||
, player_status_log.reason
|
||||
, player_status_log.expires
|
||||
FROM player_status_log
|
||||
JOIN player ON player_status_log.player_id == player.id
|
||||
JOIN player executor ON player_status_log.executor_id == executor.id
|
||||
JOIN player_status ON player_status_log.status_id == player_status.id
|
||||
WHERE LOWER(player.name) == LOWER(?)
|
||||
ORDER BY player_status_log.timestamp
|
||||
function verbana.data.get_player_status(player_id, create_if_new)
|
||||
local code = [[
|
||||
SELECT executor.id executor_id
|
||||
, executor.name executor
|
||||
, status.id status_id
|
||||
, status.name name
|
||||
, log.timestamp timestamp
|
||||
, log.reason reason
|
||||
, log.expires expires
|
||||
FROM player
|
||||
JOIN player_status_log log ON player.current_status_id == log.id
|
||||
JOIN player_status status ON log.status_id == status.id
|
||||
JOIN player executor ON log.executor_id == executor.id
|
||||
WHERE player.id == ?
|
||||
LIMIT 1
|
||||
]]
|
||||
return get_full_table(ban_record_sql, 'ban record')
|
||||
local table = get_full_ntable(code, 'get player status', player_id)
|
||||
if #table == 1 then
|
||||
return table[1]
|
||||
elseif #table > 1 then
|
||||
verbana.log('error', 'somehow got more than 1 result when getting current player status for %s', player_id)
|
||||
return nil
|
||||
elseif not create_if_new then
|
||||
return nil
|
||||
end
|
||||
if not verbana.data.set_player_status(player_id, verbana.data.verbana_player_id, verbana.data.player_status.unknown.id, 'creating initial player status') then
|
||||
verbana.log('error', 'failed to set initial player status')
|
||||
return nil
|
||||
end
|
||||
return {
|
||||
executor_id=verbana.data.verbana_player_id,
|
||||
executor=verbana.data.verbana_player,
|
||||
status_id=verbana.data.player_status.unknown.id,
|
||||
name='unknown',
|
||||
timestamp=os.clock()
|
||||
}
|
||||
end
|
||||
function verbana.data.set_player_status(player_id, executor_id, status_id, reason, expires)
|
||||
local code = [[
|
||||
INSERT INTO player_status_log (player_id, executor_id, status_id, reason, expires, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
]]
|
||||
if not execute_bind_one(code, 'insert player status', player_id, executor_id, status_id, reason, expires, os.clock()) then return false end
|
||||
local last_id = db:last_insert_rowid()
|
||||
local code = 'UPDATE player SET current_status_id = ? WHERE id = ?'
|
||||
if not execute_bind_one(code, 'update player last status id', last_id, player_id) then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
function verbana.data.get_ip_status(ipint, create_if_new)
|
||||
local code = [[
|
||||
SELECT executor.id executor_id
|
||||
, executor.name executor
|
||||
, status.id status_id
|
||||
, status.name name
|
||||
, log.timestamp timestamp
|
||||
, log.reason reason
|
||||
, log.expires expires
|
||||
FROM ip
|
||||
JOIN ip_status_log log ON ip.current_status_id == log.id
|
||||
JOIN ip_status status ON log.status_id == status.id
|
||||
JOIN player executor ON log.executor_id == executor.id
|
||||
WHERE ip.ip == ?
|
||||
LIMIT 1
|
||||
]]
|
||||
local table = get_full_ntable(code, 'get ip status', ipint)
|
||||
if #table == 1 then
|
||||
return table[1]
|
||||
elseif #table > 1 then
|
||||
verbana.log('error', 'somehow got more than 1 result when getting current ip status for %s', ipint)
|
||||
return nil
|
||||
elseif not create_if_new then
|
||||
return nil
|
||||
end
|
||||
if not verbana.data.set_ip_status(ipint, verbana.data.verbana_player_id, verbana.data.ip_status.default.id, 'creating initial ip status') then
|
||||
verbana.log('error', 'failed to set initial ip status')
|
||||
return nil
|
||||
end
|
||||
return {
|
||||
executor_id=verbana.data.verbana_player_id,
|
||||
executor=verbana.data.verbana_player,
|
||||
status_id=verbana.data.ip_status.default.id,
|
||||
name='default',
|
||||
timestamp=os.clock()
|
||||
}
|
||||
end
|
||||
function verbana.data.set_ip_status(ipint, executor_id, status_id, reason, expires)
|
||||
local code = [[
|
||||
INSERT INTO ip_status_log (ip, executor_id, status_id, reason, expires, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
]]
|
||||
if not execute_bind_one(code, 'insert player status', ipint, executor_id, status_id, reason, expires, os.clock()) then return false end
|
||||
local last_id = db:last_insert_rowid()
|
||||
local code = 'UPDATE ip SET current_status_id = ? WHERE ip = ?'
|
||||
if not execute_bind_one(code, 'update ip last status id', last_id, ipint) then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
function verbana.data.get_asn_status(asn, create_if_new)
|
||||
local code = [[
|
||||
SELECT executor.id executor_id
|
||||
, executor.name executor
|
||||
, status.id status_id
|
||||
, status.name name
|
||||
, log.timestamp timestamp
|
||||
, log.reason reason
|
||||
, log.expires expires
|
||||
FROM asn
|
||||
JOIN asn_status_log log ON asn.current_status_id == log.id
|
||||
JOIN asn_status status ON log.status_id == status.id
|
||||
JOIN player executor ON log.executor_id == executor.id
|
||||
WHERE asn.asn == ?
|
||||
LIMIT 1
|
||||
]]
|
||||
local table = get_full_ntable(code, 'get asn status', asn)
|
||||
if #table == 1 then
|
||||
return table[1]
|
||||
elseif #table > 1 then
|
||||
verbana.log('error', 'somehow got more than 1 result when getting current asn status for %s', asn)
|
||||
return nil
|
||||
elseif not create_if_new then
|
||||
return nil
|
||||
end
|
||||
if not verbana.data.set_asn_status(asn, verbana.data.verbana_player_id, verbana.data.asn_status.default.id, 'creating initial asn status') then
|
||||
verbana.log('error', 'failed to set initial asn status')
|
||||
return nil
|
||||
end
|
||||
return {
|
||||
executor_id=verbana.data.verbana_player_id,
|
||||
executor=verbana.data.verbana_player,
|
||||
status_id=verbana.data.asn_status.default.id,
|
||||
name='default',
|
||||
timestamp=os.clock()
|
||||
}
|
||||
end
|
||||
function verbana.data.set_asn_status(asn, executor_id, status_id, reason, expires)
|
||||
local code = [[
|
||||
INSERT INTO asn_status_log (asn, executor_id, status_id, reason, expires, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
]]
|
||||
if not execute_bind_one(code, 'insert player status', asn, executor_id, status_id, reason, expires, os.clock()) then return false end
|
||||
local last_id = db:last_insert_rowid()
|
||||
local code = 'UPDATE asn SET current_status_id = ? WHERE asn = ?'
|
||||
if not execute_bind_one(code, 'update asn last status id', last_id, asn) then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
function verbana.data.log(player_id, ipint, asn, success)
|
||||
local code = [[
|
||||
INSERT INTO connection_log (player_id, ip, asn, success, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
]]
|
||||
if not execute_bind_one(code, 'log connection', player_id, ipint, asn, success, os.clock()) then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
function verbana.data.assoc(player_id, ipint, asn)
|
||||
local code = [[
|
||||
INSERT OR IGNORE INTO assoc (player_id, ip, asn)
|
||||
VALUES (?, ?, ?)
|
||||
]]
|
||||
if not execute_bind_one(code, 'associate', player_id, ipint, asn, os.clock()) then return false end
|
||||
return true
|
||||
end
|
||||
function verbana.data.has_asn_assoc(player_id, asn)
|
||||
local code = 'SELECT 1 FROM assoc WHERE player_id = ? AND asn == ? LIMIT 1'
|
||||
local table = get_full_table(code, 'find player asn assoc', player_id, asn)
|
||||
return #table == 1
|
||||
end
|
||||
function verbana.data.has_ip_assoc(player_id, ipint)
|
||||
local code = 'SELECT 1 FROM assoc WHERE player_id = ? AND ip == ? LIMIT 1'
|
||||
local table = get_full_table(code, 'find player asn assoc', player_id, ipint)
|
||||
return #table == 1
|
||||
end
|
||||
|
||||
function verbana.data.get_player_status_log(player_name)
|
||||
local code = [[
|
||||
SELECT executor.name executor
|
||||
, status.name status
|
||||
, log.timestamp timestamp
|
||||
, log.reason reason
|
||||
, log.expires expires
|
||||
FROM player_status_log log
|
||||
JOIN player ON log.player_id == player.id
|
||||
JOIN player executor ON log.executor_id == executor.id
|
||||
JOIN player_status status ON log.status_id == status.id
|
||||
WHERE LOWER(player.name) == LOWER(?)
|
||||
ORDER BY log.timestamp
|
||||
]]
|
||||
return get_full_ntable(code, 'player status log', player_name)
|
||||
end
|
||||
function verbana.data.get_ip_status_log(ipint)
|
||||
local code = [[
|
||||
SELECT executor.name executor
|
||||
, status.name status
|
||||
, log.timestamp timestamp
|
||||
, log.reason reason
|
||||
, log.expires expires
|
||||
FROM ip_status_log log
|
||||
JOIN player executor ON log.executor_id == executor.id
|
||||
JOIN ip_status status ON log.status_id == status.id
|
||||
WHERE log.ip == ?
|
||||
ORDER BY log.timestamp
|
||||
]]
|
||||
return get_full_ntable(code, 'ip status log', ipint)
|
||||
end
|
||||
function verbana.data.get_asn_status_log(asn)
|
||||
local code = [[
|
||||
SELECT executor.name executor
|
||||
, status.name status
|
||||
, log.timestamp timestamp
|
||||
, log.reason reason
|
||||
, log.expires expires
|
||||
FROM asn_status_log log
|
||||
JOIN player executor ON log.executor_id == executor.id
|
||||
JOIN asn_status status ON log.status_id == status.id
|
||||
WHERE log.asn == ?
|
||||
ORDER BY log.timestamp
|
||||
]]
|
||||
return get_full_ntable(code, 'asn status log', asn)
|
||||
end
|
||||
|
||||
function verbana.data.get_player_connection_log(player_name, limit)
|
||||
local code = [[
|
||||
SELECT log.ip ip
|
||||
, log.asn asn
|
||||
, log.success success
|
||||
, log.timestamp timestamp
|
||||
, ip_status_log.status_id ip_status_id
|
||||
, asn_status_log.status_id asn_status_id
|
||||
FROM connection_log log
|
||||
JOIN player ON player.id == log.player_id
|
||||
JOIN ip ON ip.ip == log.ip
|
||||
LEFT JOIN ip_status_log ON ip.current_status_id == ip_status_log.id
|
||||
JOIN asn ON asn.asn == log.asn
|
||||
LEFT JOIN asn_status_log ON asn.current_status_id == asn_status_log.id
|
||||
WHERE LOWER(player.name) == LOWER(?)
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
]]
|
||||
if not limit or type(limit) ~= 'number' or limit < 0 then
|
||||
limit = 20
|
||||
end
|
||||
local t = get_full_ntable(code, 'player connection log', player_name, limit)
|
||||
return verbana.util.table_reversed(t)
|
||||
end
|
||||
function verbana.data.get_ip_connection_log(ipint, limit)
|
||||
local code = [[
|
||||
SELECT player.name player_name
|
||||
, player.id player_id
|
||||
, log.asn asn
|
||||
, log.success success
|
||||
, log.timestamp timestamp
|
||||
, player_status_log.status_id player_status_id
|
||||
, asn_status_log.status_id asn_status_id
|
||||
FROM connection_log log
|
||||
JOIN player ON player.id == log.player_id
|
||||
LEFT JOIN player_status_log ON player_status_log.id == player.current_status_id
|
||||
JOIN asn ON asn.asn == log.asn
|
||||
LEFT JOIN asn_status_log ON asn.current_status_id == asn_status_log.id
|
||||
WHERE log.ip == ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
]]
|
||||
if not limit or type(limit) ~= 'number' or limit < 0 then
|
||||
limit = 20
|
||||
end
|
||||
local t = get_full_ntable(code, 'ip connection log', ipint, limit)
|
||||
return verbana.util.table_reversed(t)
|
||||
end
|
||||
function verbana.data.get_asn_connection_log(asn, limit)
|
||||
local code = [[
|
||||
SELECT player.name player_name
|
||||
, player.id player_id
|
||||
, log.ip ip
|
||||
, log.success success
|
||||
, log.timestamp timestamp
|
||||
, player_status_log.status_id player_status_id
|
||||
, ip.current_status_id ip_status_id
|
||||
FROM connection_log log
|
||||
JOIN player ON player.id == log.player_id
|
||||
LEFT JOIN player_status_log ON player_status_log.id == player.current_status_id
|
||||
JOIN ip ON ip.ip == log.ip
|
||||
LEFT JOIN ip_status_log ON ip.current_status_id == ip_status_log.id
|
||||
WHERE log.asn == ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
]]
|
||||
if not limit or type(limit) ~= 'number' or limit < 0 then
|
||||
limit = 20
|
||||
end
|
||||
local t = get_full_ntable(code, 'asn connection log', asn, limit)
|
||||
return verbana.util.table_reversed(t)
|
||||
end
|
||||
|
||||
function verbana.data.get_player_associations(player_name)
|
||||
local code = [[
|
||||
SELECT assoc.ip ip
|
||||
, assoc.asn asn
|
||||
, ip_status_log.status_id ip_status_id
|
||||
, asn_status_log.status_id asn_status_id
|
||||
FROM assoc
|
||||
JOIN player ON player.id == assoc.player_id
|
||||
JOIN ip ON ip.ip == log.ip
|
||||
LEFT JOIN ip_status_log ON ip.current_status_id == ip_status_log.id
|
||||
JOIN asn ON asn.asn == log.asn
|
||||
LEFT JOIN asn_status_log ON asn.current_status_id == asn_status_log.id
|
||||
WHERE player.name == ?
|
||||
ORDER BY assoc.asn, assoc.ip
|
||||
]]
|
||||
return get_full_ntable(code, 'player associations', player_name)
|
||||
end
|
||||
function verbana.data.get_ip_associations(ipint)
|
||||
local code = [[
|
||||
SELECT
|
||||
DISTINCT player.name player_name
|
||||
, player_status_log.status_id player_status_id
|
||||
FROM assoc
|
||||
JOIN player ON player.id == assoc.player_id
|
||||
LEFT JOIN player_status_log ON player_status_log.id == player.current_status_id
|
||||
WHERE assoc.ip == ?
|
||||
ORDER BY LOWER(player.name)
|
||||
]]
|
||||
return get_full_ntable(code, 'ip associations', ipint)
|
||||
end
|
||||
function verbana.data.get_asn_associations(asn)
|
||||
local code = [[
|
||||
SELECT
|
||||
DISTINCT player.name player_name
|
||||
, player_status_log.status_id player_status_id
|
||||
FROM assoc
|
||||
JOIN player ON player.id == assoc.player_id
|
||||
LEFT JOIN player_status_log ON player_status_log.id == player.current_status_id
|
||||
WHERE assoc.asn == ?
|
||||
ORDER BY LOWER(player.name)
|
||||
]]
|
||||
return get_full_ntable(code, 'asn associations', asn)
|
||||
end
|
||||
|
||||
function verbana.data.get_player_cluster(player_name)
|
||||
local code = [[
|
||||
SELECT
|
||||
DISTINCT other.name player_name
|
||||
, player_status_log.status_id status_id
|
||||
FROM player
|
||||
JOIN assoc player_assoc ON player.id == player_assoc.player_id
|
||||
JOIN assoc other_assoc ON player_assoc.ip == other_assoc.ip
|
||||
AND player_assoc.player_id != other_assoc.player_id
|
||||
JOIN player other ON other_assoc.player_id == other.id
|
||||
LEFT JOIN player_status_log ON other.id == player_status_log.player_id
|
||||
WHERE player.name == ?
|
||||
ORDER BY other_name
|
||||
]]
|
||||
return get_full_ntable(code, 'player cluster', player_name)
|
||||
end
|
||||
|
||||
function verbana.data.get_all_banned_players()
|
||||
local code = [[
|
||||
SELECT player.name player_name
|
||||
, player_status_log.status_id player_status_id
|
||||
, player_status_log.reason reason
|
||||
, player_status_log.expires expires
|
||||
FROM player
|
||||
LEFT JOIN player_status_log ON player.id == player_status_log.player_id
|
||||
WHERE player_status_log.status_id IN (4, 5, 6)
|
||||
]] -- TODO: ... hard-coded values? ...
|
||||
return get_full_ntable(code, 'all banned')
|
||||
end
|
||||
-- TODO: methods to get logs of player_status, ip_status, asn_status
|
||||
-- TODO: methods to get connection logs by player, ip, asn
|
||||
-- TODO: methods to get association logs by player, ip, asn
|
||||
|
|
4
init.lua
4
init.lua
|
@ -1,3 +1,4 @@
|
|||
if false then
|
||||
verbana = {}
|
||||
verbana.version = '1.0.0'
|
||||
local modname = minetest.get_current_modname()
|
||||
|
@ -51,5 +52,4 @@ sqlite3 = nil
|
|||
verbana.ie = nil
|
||||
verbana.sql = nil
|
||||
verbana.db = nil
|
||||
|
||||
|
||||
end
|
||||
|
|
|
@ -117,7 +117,7 @@ minetest.register_on_prejoinplayer(function(name, ipstr)
|
|||
local has_assoc = verbana.data.has_asn_assoc(player_id, asn) or verbana.data.has_ip_assoc(player_id, ipint)
|
||||
if not has_assoc then
|
||||
-- note: if 'suspicious' is true, then 'return_value' should be nil before this
|
||||
return_value = ('Suspicious activity detected; connection denied.'):format()
|
||||
return_value = 'Suspicious activity detected.'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -179,7 +179,7 @@ minetest.register_on_newplayer(function(player)
|
|||
verbana.data.set_player_status(
|
||||
player_id,
|
||||
verbana.data.verbana_player_id,
|
||||
verbana.data.player_status_id.default,
|
||||
verbana.data.player_status.default.id,
|
||||
'new player'
|
||||
)
|
||||
verbana.log('action', 'new player %s', name)
|
||||
|
@ -195,3 +195,35 @@ minetest.register_on_joinplayer(function(player)
|
|||
verbana.chat.tell_mods(('*** Player %s from A%s (%s) is unverified.'):format(name, asn, asn_description))
|
||||
end
|
||||
end)
|
||||
|
||||
if verbana.settings.verification_jail and verbana.settings.verification_jail_period then
|
||||
local timer = 0
|
||||
local verification_jail = verbana.settings.verification_jail
|
||||
local check_player_privs = minetest.check_player_privs
|
||||
local function should_rejail(player)
|
||||
local name = player:get_player_name()
|
||||
if not check_player_privs(name, {unverified = true}) then
|
||||
return false
|
||||
end
|
||||
local pos = player:get_pos()
|
||||
return not (
|
||||
verification_jail.x[1] <= pos.x and pos.x <= verification_jail.x[2] and
|
||||
verification_jail.y[1] <= pos.y and pos.y <= verification_jail.y[2] and
|
||||
verification_jail.z[1] <= pos.z and pos.z <= verification_jail.z[2]
|
||||
)
|
||||
end
|
||||
local verification_pos = verbana.settings.verification_pos
|
||||
local verification_jail_period = verbana.settings.verification_jail_period
|
||||
minetest.register_globalstep(function(dtime)
|
||||
timer = timer + dtime;
|
||||
if timer < verification_jail_period then
|
||||
return
|
||||
end
|
||||
timer = 0
|
||||
for _, player in ipairs(minetest.get_connected_players()) do
|
||||
if should_rejail(player) then
|
||||
player:set_pos(verification_pos)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
|
28
schema.sql
28
schema.sql
|
@ -20,13 +20,13 @@ CREATE TABLE IF NOT EXISTS player (
|
|||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
, name TEXT NOT NULL
|
||||
, main_player_id INTEGER
|
||||
, last_status_id INTEGER
|
||||
, current_status_id INTEGER
|
||||
, FOREIGN KEY (main_player_id) REFERENCES player(id)
|
||||
, FOREIGN KEY (last_status_id) REFERENCES player_status_log(id)
|
||||
, FOREIGN KEY (current_status_id) REFERENCES player_status_log(id)
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS player_name ON player(name);
|
||||
CREATE INDEX IF NOT EXISTS player_main_player_id ON player(main_player_id);
|
||||
CREATE INDEX IF NOT EXISTS player_last_status_id ON player(last_status_id);
|
||||
CREATE INDEX IF NOT EXISTS player_current_status_id ON player(current_status_id);
|
||||
INSERT OR IGNORE INTO player (name) VALUES ('!verbana!');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS player_status_log (
|
||||
|
@ -62,10 +62,10 @@ CREATE TABLE IF NOT EXISTS ip_status (
|
|||
|
||||
CREATE TABLE IF NOT EXISTS ip (
|
||||
ip INTEGER PRIMARY KEY
|
||||
, last_status_id INTEGER
|
||||
, FOREIGN KEY (last_status_id) REFERENCES ip_status_log(id)
|
||||
, current_status_id INTEGER
|
||||
, FOREIGN KEY (current_status_id) REFERENCES ip_status_log(id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ip_last_status_id ON ip(last_status_id);
|
||||
CREATE INDEX IF NOT EXISTS ip_current_status_id ON ip(current_status_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ip_status_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
|
@ -98,10 +98,10 @@ CREATE TABLE IF NOT EXISTS asn_status (
|
|||
|
||||
CREATE TABLE IF NOT EXISTS asn (
|
||||
asn INTEGER PRIMARY KEY
|
||||
, last_status_id INTEGER
|
||||
, FOREIGN KEY (last_status_id) REFERENCES asn_status_log(id)
|
||||
, current_status_id INTEGER
|
||||
, FOREIGN KEY (current_status_id) REFERENCES asn_status_log(id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS asn_last_status_id ON asn(last_status_id);
|
||||
CREATE INDEX IF NOT EXISTS asn_current_status_id ON asn(current_status_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS asn_status_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
|
@ -120,7 +120,7 @@ CREATE TABLE IF NOT EXISTS asn_status_log (
|
|||
CREATE INDEX IF NOT EXISTS asn_status_log_reason ON asn_status_log(reason);
|
||||
-- END ASN
|
||||
-- OTHER
|
||||
CREATE TABLE IF NOT EXISTS log (
|
||||
CREATE TABLE IF NOT EXISTS connection_log (
|
||||
player_id INTEGER NOT NULL
|
||||
, ip INTEGER NOT NULL
|
||||
, asn INTEGER NOT NULL
|
||||
|
@ -131,10 +131,10 @@ CREATE TABLE IF NOT EXISTS log (
|
|||
, FOREIGN KEY (asn) REFERENCES asn(asn)
|
||||
, UNIQUE (player_id, ip, success, timestamp)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS log_player ON log(player_id);
|
||||
CREATE INDEX IF NOT EXISTS log_ip ON log(ip);
|
||||
CREATE INDEX IF NOT EXISTS log_asn ON log(asn);
|
||||
CREATE INDEX IF NOT EXISTS log_timestamp ON log(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS log_player ON connection_log(player_id);
|
||||
CREATE INDEX IF NOT EXISTS log_ip ON connection_log(ip);
|
||||
CREATE INDEX IF NOT EXISTS log_asn ON connection_log(asn);
|
||||
CREATE INDEX IF NOT EXISTS log_timestamp ON connection_log(timestamp);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assoc (
|
||||
player_id INTEGER
|
||||
|
|
|
@ -13,9 +13,12 @@ end
|
|||
|
||||
-- TODO load priv settings
|
||||
|
||||
verbana.settings.verified_privs = ...
|
||||
verbana.settings.unverified_privs = settings_get_set('...', {unverified = true, shout = true})
|
||||
verbana.settings.privs_to_whitelist = nil
|
||||
verbana.settings.universal_verification = false -- TODO this should be loaded from mod_storage?
|
||||
|
||||
verbana.settings.spawn_pos = {x = 111, y = 13, z = -507} -- TODO this should be grabbed from the spawn mod
|
||||
verbana.settings.verification_pos = {x = 172, y = 29, z = -477} -- TODO load from config
|
||||
verbana.settings.verification_jail = {x={159, 184}, y={27, 36}, z={-493, -472}}
|
||||
verbana.settings.verification_jail_period = nil
|
||||
|
|
17
util.lua
17
util.lua
|
@ -16,3 +16,20 @@ function verbana.util.table_invert(t)
|
|||
for k,v in pairs(t) do inverted[v] = k end
|
||||
return inverted
|
||||
end
|
||||
|
||||
function verbana.util.table_reversed(t)
|
||||
local len = #t
|
||||
local reversed = {}
|
||||
for i = len,1,-1 do
|
||||
reversed[len - i + 1] = t[i]
|
||||
end
|
||||
return reversed
|
||||
end
|
||||
|
||||
function verbana.util.table_contains(t, value)
|
||||
for _, v in ipairs(t) do
|
||||
if v == value then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue