Refactor mod to use the new HTTPS API.

This commit makes several changes:
 ¤ The mod (finally) uses the lurkcoinV3 API.
 ¤ Transactions to non-existent users are reverted (provided both 
servers are using the updated mod)
   · There is a small processing fee on transactions to invalid users 
(definitely not a bug)
 ¤ Fix non-minetest_game formspec prepends on ATM formspecs.
 ¤ Stop overriding minetest.log.
 ¤ Add luacheck workflow based on the one at 
https://github.com/BlockySurvival/bls_custom
 ¤ Fix errors reported by luacheck.
This commit is contained in:
luk3yx 2020-10-04 17:19:46 +13:00
parent 640fbae0b3
commit 50f5b41ccd
6 changed files with 214 additions and 188 deletions

13
.github/workflows/luacheck.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: luacheck
on: [push, pull_request]
jobs:
luacheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run luacheck
uses: Roang-zero1/factorio-mod-luacheck@master
with:
luacheckrc_url: https://raw.githubusercontent.com/luk3yx/minetest-lurkcoin/master/.luacheckrc

19
.luacheckrc Normal file
View File

@ -0,0 +1,19 @@
-- Originally from bls_custom
max_line_length = 80
globals = {
'atm',
'accounts',
'default',
'economy',
'lurkcoin',
'minetest',
'money',
'ItemStack'
}
read_globals = {
string = {fields = {'split', 'trim'}},
table = {fields = {'copy'}},
}

View File

@ -14,30 +14,34 @@ local formspecs = {}
-- 0.4 compatibility
lurkcoin.formspec_prepend = ''
if minetest.get_modpath('default') and rawget(_G, 'default') and
default.gui_bg and default.gui_bg_img and default.gui_slots then
if minetest.global_exists('default') and default.gui_bg and
default.gui_bg_img and default.gui_slots then
lurkcoin.formspec_prepend = default.gui_bg .. default.gui_bg_img ..
default.gui_slots
end
-- The formspec code is based on something random I did in 2017(?) for
-- lurkcoinV1, formspecs are weird and I somehow got it right™ then.
local function centre_label(pos, label)
return 'image_button[' .. pos .. ';blank.png;;' .. e(label) ..
';true;false;]'
end
-- The formspec code is based on something I did in 2017(?) for lurkcoinV1,
-- formspecs are weird and I somehow got it right then.
local function get_formspec(name, page, params)
-- The formspec template
local formspec = 'size[8,9;]' .. lurkcoin.formspec_prepend ..
local formspec = 'size[8,9]' .. lurkcoin.formspec_prepend ..
'label[0.5,1.75;Your balance: ' ..
e(lurkcoin.bank.getbal(name)) .. 'cr.]' ..
'image_button[2,0.55;4,0.5;default_dirt.png^\\[colorize:#343434;y;' ..
'Welcome to a ' .. e(lurkcoin.server_name) .. ' ATM!]' ..
'label[0.5,2.25;Exchange rate: \194\1641.00 is equal to ' ..
e(lurkcoin.exchange_rate) .. 'cr.]' ..
'image_button[1.75,1.05;4.5,0.5;default_dirt.png^\\[colorize:' ..
'#343434;y; Your account: ' .. e(name) .. ']' ..
'image[0.5,0.5;1,1;default_mese_crystal.png]' ..
centre_label('1,0.55;6,0.5', 'Welcome to a ' .. lurkcoin.server_name ..
'ATM!') ..
'label[0.5,2.25;Exchange rate: \194\1641.00 is equal to ' ..
e(lurkcoin.exchange_rate) .. 'cr.]' ..
centre_label('1.75,1.05;4.5,0.5', 'Your account: ' .. name) ..
'image[0.5,0.5;1,1;default_mese_crystal.png]' ..
'image[6.5,0.5;1,1;default_mese_crystal.png]'
-- Get the page formspec
local page = formspecs[page] or formspecs.main
page = formspecs[page] or formspecs.main
if type(page) == 'string' then
formspec = formspec .. page
elseif type(page) == 'function' then
@ -52,7 +56,7 @@ end
local withdrawls = false
-- Payment screen
function formspecs.pay(name, fields, guessed_amount)
function formspecs.pay(name, fields)
fields = fields or {}
local formspec =
'field[0.8,3.5;7,1;user;User to pay;' .. e(fields.user or '') .. ']' ..
@ -93,7 +97,7 @@ function formspecs.pay(name, fields, guessed_amount)
exc = tostring(math.floor(exc * 100) / 100)
formspec = formspec .. '\n' .. fields.amount .. 'cr is ' ..
'equal to \194\164' .. exc .. '.'
'equal to ' .. exc .. '.'
end
formspec = formspec .. ']' ..
'button[0.5,8;3.5,1;payuser;Cancel]' ..
@ -140,16 +144,20 @@ if minetest.get_modpath('currency') then
}
-- Remove non-registered notes
local remove = {}
for id, note in pairs(withdrawls) do
if not minetest.registered_items[note] then
withdrawls[id] = nil
table.insert(remove, id)
end
end
for _, id in ipairs(remove) do
withdrawls[id] = nil
end
assert(#withdrawls > 0, 'The "currency" mod did not register any notes!')
-- Create a detached inventory for depositing
local inv = minetest.create_detached_inventory('lurkcoin:atm_deposit', {
on_put = function(inv, listname, index, stack, player)
on_put = function(inv, listname, _, stack, player)
local name = stack:get_name()
if name:sub(1, 17) == 'currency:minegeld' then
local m = name:sub(19)
@ -157,6 +165,8 @@ if minetest.get_modpath('currency') then
if m:sub(1, 5) == 'cent_' then
m = tonumber(m:sub(6))
if m then m = m / 100 end
elseif m == 'bundle' then
m = 0.05 * 9
else
m = tonumber(m)
end
@ -170,16 +180,16 @@ if minetest.get_modpath('currency') then
if not lurkcoin.bank.add(pname, m, 'Deposit') then
player:get_inventory():add_item('main', stack)
end
core.log('action', 'Player ' .. pname .. ' deposits ' ..
tostring(m) .. 'Mg (' .. stack:to_string() ..
') into a lurkcoin ATM.')
minetest.log('action', 'Player ' .. pname ..
' deposits ' .. tostring(m) .. 'Mg (' ..
stack:to_string() .. ') into a lurkcoin ATM.')
end
end
inv:set_list(listname, {})
lurkcoin.show_atm(player:get_player_name(), 'deposit')
end,
allow_put = function(inv, listname, index, stack, player)
allow_put = function(_, _, _, stack, _)
local name = stack:get_name()
if name:sub(1, 17) == 'currency:minegeld' then
return stack:get_count()
@ -198,8 +208,7 @@ if minetest.get_modpath('currency') then
'list[current_player;main;0,6.08;8,3;8]' ..
'list[detached:lurkcoin:atm_deposit;lurkcoin;3.5,3;1,1;]' ..
'listring[]' ..
'image_button[0,3;3.5,1;default_dirt.png^\\[colorize:#343434;' ..
'ignore;Deposit money here →]' ..
centre_label('0,3;3.5,1', 'Deposit money here →') ..
'button[5.25,3;2,1;home;Finish]'
else
-- When there is no physical currency, the only thing you can do is pay
@ -208,19 +217,17 @@ else
end
-- "Transaction accepted" screen
function formspecs.success(name, text)
function formspecs.success(_, text)
return 'image[2.1,3;4.5,4.5;lurkcoin_success.png]' ..
'image_button[1,7;6,1;default_dirt.png^\\[colorize:#343434;' ..
'ignore;' .. e(text or 'Transaction sent!') .. ']' ..
centre_label('1,7;6,1', text or 'Transaction sent!') ..
'button[0.5,8;3.5,1;home;Go back]' ..
'button_exit[4,8;3.5,1;quit;Quit]'
end
-- "Transaction failed" screen
function formspecs.error(name, text)
function formspecs.error(_, text)
return 'image[2.1,3;4.5,4.5;lurkcoin_error.png]' ..
'image_button[1,7;6,1;default_dirt.png^\\[colorize:#343434;' ..
'ignore;' .. e(text or 'An error has occurred!') .. ']' ..
centre_label('1,7;6,1', text or 'An error has occurred!') ..
'button[0.5,8;3.5,1;home;Go back]' ..
'button_exit[4,8;3.5,1;quit;Quit]'
end
@ -228,10 +235,8 @@ end
-- Processing screen
formspecs.processing =
'image[2.1,3;4.5,4.5;lurkcoin_processing.png]' ..
'image_button[1,7;6,1;default_dirt.png^\\[colorize:#343434;' ..
'ignore;Processing your transaction...]' ..
'image_button[1,8;6,1;default_dirt.png^\\[colorize:#343434;' ..
'ignore;This should only take a few seconds.]'
centre_label('1,7;6,1', 'Processing your transaction...') ..
centre_label('1,8;6,1', 'This should only take a few seconds.')
-- A wrapper function
function lurkcoin.show_atm(name, page, params)
@ -267,10 +272,12 @@ minetest.register_on_player_receive_fields(function(player, formname, raw)
'ERROR: You cannot afford to do that!')
end
local note = false
local note
for id, item in pairs(withdrawls) do
-- HACK: Multiply both amount and id by 1000 to avoid
-- floating-point bugs.
if (not note or id > note) and
math.floor(amount / id) * id == amount then
(amount * 1000) % (id * 1000) == 0 then
local def = minetest.registered_items[item]
if def and amount / id <= (def.stack_max or 99) then
note = id
@ -286,7 +293,7 @@ minetest.register_on_player_receive_fields(function(player, formname, raw)
local stack = ItemStack({
name = withdrawls[note],
count = amount / note,
count = (amount * 1000) / (note * 1000),
})
local inv = player:get_inventory()
if not inv:room_for_item('main', stack) then
@ -372,14 +379,5 @@ minetest.register_on_player_receive_fields(function(player, formname, raw)
end)
minetest.register_on_leaveplayer(function(player)
-- TODO: I forgot what one register_on_leaveplayer uses and am too lazy to
-- check.
local name
if type(player) == 'string' then
name = player
else
name = player:get_player_name()
end
open_atms[name] = nil
open_atms[player:get_player_name()] = nil
end)

View File

@ -22,7 +22,7 @@ minetest.register_node('lurkcoin:atm', {
end
end,
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
on_rightclick = function(_, _, clicker, _, _)
return lurkcoin.show_atm(clicker:get_player_name())
end,
})

254
core.lua
View File

@ -4,28 +4,40 @@
-- © 2019 by luk3yx
--
lurkcoin.version = 0
-- Change this if you are hosting your own lurkcoin-core instance
local baseurl = 'https://us.xeroxirc.net:7000'
lurkcoin.version = 3
lurkcoin.timeout = 10
lurkcoin.exchange_rate = 1
-- Do not allow other mods to modify this, or they may be able to bypass mod
-- security restrictions!
local baseurl = 'https://us.xeroxirc.net:7000/v2'
local function log(level, text)
if text then
text = text:gsub('[\r\n]', ' ')
else
level, text = 'action', level:gsub('[\r\n]', ' ')
end
return minetest.log(level, '[lurkcoin] ' .. text)
end
local function logf(text, ...)
return log(text:format(...))
end
-- Get the username and API token
lurkcoin.server_name = minetest.settings:get('lurkcoin.username')
local token = minetest.settings:get('lurkcoin.token')
local token = minetest.settings:get('lurkcoin.token')
-- Make sure lurkcoin.server_name exists
if not lurkcoin.server_name then
lurkcoin.server_name = '<unknown>'
minetest.log('warning', 'lurkcoin has no server name set!')
log('warning', 'lurkcoin.server_name is not set!')
end
-- Make sure the HTTP API exists
local http = ...
if not http then
minetest.log('warning', 'lurkcoin is not allowed to use the HTTP API! ' ..
log('warning', 'lurkcoin is not allowed to use the HTTP API! ' ..
'Please add lurkcoin to secure.http_mods in minetest.conf.')
end
@ -34,177 +46,185 @@ end
lurkcoin.user_agent = 'Minetest ' .. minetest.get_version().string ..
' (with lurkcoin mod v' .. tostring(lurkcoin.version) .. ')'
-- Create the HTTP header list
local headers
if token then
local raw = lurkcoin.server_name:gsub(':', '_') .. ':' .. token
local auth = minetest.encode_base64(raw)
headers = {
-- minetest.encode_base64() doesn't add padding
'Authorization: Basic ' .. auth .. ('='):rep(4 - #auth % 4),
'Content-Type: application/json',
'X-Force-OK: true'
}
end
local function E(code, msg)
return {sucess = false, error = code, message = msg}
end
-- Download functions
local function get(url, data, callback)
-- To prevent race conditions, these callbacks wait until at least the next
-- globalstep.
if not data then
data = {}
elseif not http then
minetest.after(0, callback, {
completed = true,
succeeded = false,
timeout = true,
code = 500,
data = 'ERROR: The lurkcoin mod is not in secure.http_mods!'
})
if not http then
minetest.after(0, callback, E('ERR_CONNECTIONFAILED',
'The lurkcoin mod is not in secure.http_mods!'))
return
elseif not lurkcoin.server_name or not token then
minetest.after(0, callback, {
completed = true,
succeeded = false,
timeout = true,
code = 401,
data = 'ERROR: The lurkcoin mod does not have (correct) ' ..
'account credentials!'
})
elseif not headers then
minetest.after(0, callback, E('ERR_INVALIDLOGIN',
'The lurkcoin mod does not have (correct) credentials!'))
return
end
data.name = lurkcoin.server_name
data.token = token
-- Minetest eats any non-200 code.
data.force_200 = '200'
http.fetch({
url = baseurl .. '/' .. url,
timeout = lurkcoin.timeout,
post_data = data,
user_agent = lurkcoin.user_agent,
}, function(res)
if res.timeout then
minetest.log('warning', '[lurkcoin] Could not connect to lurkcoin!')
res.code = 500
res.data = 'ERROR: Could not connect to lurkcoin!'
elseif res.code == 401 or res.code == 418 then
minetest.log('warning', '[lurkcoin] Invalid username or API token!')
lurkcoin.server_name, token = '<unknown>', nil
elseif res.code == 200 and res.data:sub(1, 7) == 'ERROR: ' then
res.code = 501
url = baseurl .. '/v3/' .. url,
timeout = lurkcoin.timeout,
post_data = data and minetest.write_json(data),
user_agent = lurkcoin.user_agent,
extra_headers = headers
}, function(http_res)
local res
if http_res.timeout then
log('warning', 'Could not connect to lurkcoin!')
res = E('ERR_CONNECTIONFAILED', 'Could not connect to lurkcoin!')
else
res = minetest.parse_json(http_res.data)
if type(data) ~= 'table' then data = nil end
end
local success, msg = pcall(callback, res)
if not success then
minetest.log('error', '[lurkcoin] Error: ' .. msg)
local ok, msg = pcall(callback, res or E('ERR_UNKNOWNERROR', '???'))
if not ok then
log('error', msg)
end
end)
end
-- Process incoming transactions.
local handled_transactions = {}
local acknowledged_transactions = {}
local function _sync(res)
if res.code == 200 then
local data = minetest.parse_json(res.data)
local function sync_callback(res)
if not res.success then return end
-- Update the exchange rate
assert(type(data.exchange_rate) == 'number')
assert(data.exchange_rate == data.exchange_rate)
lurkcoin.exchange_rate = data.exchange_rate
-- Process any unprocessed transactions
for _, t in ipairs(data.transactions) do
assert(type(t[3]) == 'number')
local id = minetest.serialize(t)
if not handled_transactions[id] then
handled_transactions[id] = true
core.log('action', '[lurkcoin] ' .. t[4])
lurkcoin.bank.add(t[2], t[3])
-- Process any unprocessed transactions
local reject = {}
for _, t in ipairs(res.result) do
local id = t.id
if not acknowledged_transactions[id] then
if lurkcoin.bank.user_exists(t.target) then
acknowledged_transactions[id] = true
logf('[%s] \194\164%s (sent %s, received %scr) - ' ..
'Transaction from %q on %q to %q.',
t.id, t.amount, t.sent_amount, t.received_amount, t.source,
t.source_server, t.target)
lurkcoin.bank.add(t.target, t.received_amount)
else
logf('Rejecting transaction %s, user %q does not exist.',
t.id, t.target)
table.insert(reject, t.id)
end
end
end
-- Tell lurkcoin to remove processed transactions
if #data.transactions > 0 then
get('remove_transactions', {count = tostring(#data.transactions)},
function(res)
if res.code == 200 then
for _, t in ipairs(data.transactions) do
handled_transactions[minetest.serialize(t)] = nil
end
end
end)
-- Acknowledge transactions (if any)
if next(acknowledged_transactions) then
local ack = {}
for id, _ in pairs(acknowledged_transactions) do
table.insert(ack, id)
end
get('acknowledge_transactions', {transactions = ack}, function(res2)
if res2.success then
for _, id in ipairs(ack) do
acknowledged_transactions[id] = nil
end
end
end)
end
-- Reject transactions (if any)
if #reject > 0 then
get('reject_transactions', {transactions = reject}, function() end)
end
end
-- Periodically sync with lurkcoin.
local function sync()
get('get_transactions', {as_object = 'true'}, _sync)
get('pending_transactions', nil, sync_callback)
-- Only set lurkcoin.exchange_rate once
if not lurkcoin.exchange_rate then
get('exchange_rates', {source = lurkcoin.server_name, target = '',
amount = 1}, function(res)
if res.success and type(res.result) == 'number' then
lurkcoin.exchange_rate = res.result
end
end)
end
minetest.after(300, sync)
end
-- Start syncing once the game is loaded.
minetest.after(0, sync)
-- Get an exchange rate
function lurkcoin.get_exchange_rate(amount, to, callback)
function lurkcoin.get_exchange_rate(amount, target, callback)
assert(callback)
amount = amount and tonumber(amount)
if not amount or amount ~= amount then return callback(nil) end
get('exchange_rates', {
from = lurkcoin.server_name,
to = to or 'lurkcoin',
amount = tostring(amount),
source = lurkcoin.server_name,
target = target or '',
amount = amount,
}, function(res)
if res.code == 200 then
local amount = tonumber(res.data)
if amount == amount then return callback(amount, nil) end
end
local msg
if res.code == 502 then
msg = 'That server does not exist!'
if res.success then
callback(res.result, nil)
elseif res.error == 'ERR_TARGETSERVERNOTFOUND' then
callback(nil, 'That server does not exist!')
else
msg = res.data
callback(nil, tostring(res.message))
end
return callback(nil, tostring(msg))
end)
end
-- Pay a user (cross-server)
function lurkcoin.pay(from, to, server, amount, callback)
function lurkcoin.pay(source, target, target_server, amount, callback)
assert(type(amount) == 'number' and callback)
-- Run lurkcoin.bank.pay() if this is not a cross-server transaction.
if not server or server == '' or server:lower() ==
lurkcoin.server_name:lower() then
return callback(lurkcoin.bank.pay(from, to, amount))
if not target_server or target_server == '' or
target_server:lower() == lurkcoin.server_name:lower() then
return callback(lurkcoin.bank.pay(source, target, amount))
end
-- Sanity checks
amount = math.floor(amount * 100) / 100
if not lurkcoin.bank.user_exists(from) then
if not lurkcoin.bank.user_exists(source) then
return callback(false, 'ERROR: The "from" user does not exist!')
elseif amount ~= amount or amount <= 0 then
return callback(false, 'ERROR: Invalid number!')
elseif lurkcoin.bank.getbal(from) - amount < 0 then
elseif amount > lurkcoin.bank.getbal(source) then
return callback(false, 'ERROR: You cannot afford to do that!')
end
if not lurkcoin.bank.subtract(from, amount, 'Transaction to "' .. to ..
'" on server "' .. server .. '".') then
if not lurkcoin.bank.subtract(source, amount, 'Transaction to "' ..
source .. '" on server "' .. target_server .. '".') then
return callback(false, 'ERROR: Transaction failed!')
end
-- Send the request
return get('pay', {
target = to,
server = server,
amount = tostring(amount),
local_currency = 'true'
source = source,
target = target,
target_server = target_server,
amount = amount,
local_currency = true
}, function(res)
if res.code ~= 200 then
lurkcoin.bank.add(from, amount, 'Reverting failed transaction.')
if res.data == 'ERROR: You cannot afford to do that!' then
res.data = 'ERROR: This server cannot afford to do that!'
end
else
minetest.log('action', '[lurkcoin] User ' .. from .. ' paid ' ..
to .. ' (on server ' .. server .. ') ' .. tostring(amount) .. 'cr.')
if res.success then
logf('User %q paid %q (on server %q) %scr.', source, target,
target_server, amount)
return callback(true, 'Transaction sent!')
end
return callback(res.code == 200, res.data or 'ERROR: Unknown error!')
lurkcoin.bank.add(source, amount, 'Reverting failed transaction.')
if res.code == 'ERR_CANNOTAFFORD' then
res.message = 'This server cannot afford to do that!'
end
return callback(false, 'ERROR: ' .. tostring(res.message))
end)
end

View File

@ -26,30 +26,6 @@ dofile(modpath .. '/atm-core.lua')
-- Load the ATM blocks
dofile(modpath .. '/atm-nodes.lua')
-- Backport https://github.com/minetest/minetest/pull/8420 if required
if not minetest.get_modpath('cloaking') or not cloaking.hide_player then
table.insert(minetest.registered_on_chat_messages, 1, function(name, msg)
if msg:find('[\r\n]') then
minetest.chat_send_player(name,
'New lines are not permitted in chat messages')
return true
end
end)
-- Also tweak minetest.log because of paranoia.
local log = minetest.log
function minetest.log(level, text)
level = level:gsub('[\r\n]', ' ')
if text then
text = text:gsub('[\r\n]', ' ')
else
text = level
level = 'none'
end
return log(level, text)
end
end
-- Display loaded message
minetest.log('action', '[lurkcoin] Loaded on server "' ..
lurkcoin.server_name .. '".')