From 50f5b41ccd3f4da3ea817619efc69f652eb35e5f Mon Sep 17 00:00:00 2001 From: luk3yx Date: Sun, 4 Oct 2020 17:19:46 +1300 Subject: [PATCH] Refactor mod to use the new HTTPS API. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/luacheck.yml | 13 ++ .luacheckrc | 19 +++ atm-core.lua | 90 ++++++------ atm-nodes.lua | 2 +- core.lua | 254 ++++++++++++++++++--------------- init.lua | 24 ---- 6 files changed, 214 insertions(+), 188 deletions(-) create mode 100644 .github/workflows/luacheck.yml create mode 100644 .luacheckrc diff --git a/.github/workflows/luacheck.yml b/.github/workflows/luacheck.yml new file mode 100644 index 0000000..0e830ae --- /dev/null +++ b/.github/workflows/luacheck.yml @@ -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 diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..13e1419 --- /dev/null +++ b/.luacheckrc @@ -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'}}, +} diff --git a/atm-core.lua b/atm-core.lua index f68eba6..4e254bf 100644 --- a/atm-core.lua +++ b/atm-core.lua @@ -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) diff --git a/atm-nodes.lua b/atm-nodes.lua index 1474d5f..8a91189 100644 --- a/atm-nodes.lua +++ b/atm-nodes.lua @@ -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, }) diff --git a/core.lua b/core.lua index 92e7e26..6389438 100644 --- a/core.lua +++ b/core.lua @@ -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 = '' - 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 = '', 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 diff --git a/init.lua b/init.lua index cb7feae..33f7ec5 100644 --- a/init.lua +++ b/init.lua @@ -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 .. '".')