diff --git a/.gitignore b/.gitignore index 9b288416..b31280a2 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,11 @@ Factorio* /instances /sharedMods /database -/sharedPlugins database/items.db -config.json \ No newline at end of file +config.json + +# weird pattern to ignore sharedPlugins except not ignore some core modules +!sharedPlugins/ +sharedPlugins/* +!sharedPlugins/inventoryImports/ +!sharedPlugins/UPSdisplay/ \ No newline at end of file diff --git a/package.json b/package.json index 02f9653b..38547851 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "fs-extra": "^1.0.0", "getmac": "^1.2.1", "hash-files": "^1.1.1", + "luamin": "^1.0.4", "mkdirp": "^0.5.1", "ncp": "^2.0.0", "nedb": "^1.8.0", diff --git a/sharedPlugins/inventoryImports/index.js b/sharedPlugins/inventoryImports/index.js index 85898ccc..94a98101 100644 --- a/sharedPlugins/inventoryImports/index.js +++ b/sharedPlugins/inventoryImports/index.js @@ -1,68 +1,36 @@ const needle = require("needle"); const isJson = require("./isJson.js"); const pluginConfig = require("./config.js"); -const objectOps = require("../../lib/objectOps.js"); + +const functions = require("./interfacingFunctions"); console.log("/silent-command game.print('InventoryImports enabled')\n"); var config = {}; process.stdin.setEncoding('utf8'); -let historicalTicks = []; process.stdin.on('readable', () => { var chunk = process.stdin.read(); - if (chunk !== null && isNaN(chunk)){ - // eval because my lua is writing a JS object instead of traditional JSON - // this was done because I hate escaping strings and there are more quotes - // in JSON than JS objects. - let inventory = ""; - eval("inventory = " + chunk); - /* Example output: - {'1':{ - inventory: { - 'raw-wood': 4 - }, - requestSlots:{ - 'underground-belt': 50, - 'fast-transport-belt': 50, - 'express-underground-belt': 50 - }}} - */ + if (chunk !== null && isNaN(chunk) && config && !isJson(chunk)&& !chunk.includes("[")){ + // returns JS object, throws if there are syntax errors in input string + let inventory = functions.parseJsString(chunk); console.log(inventory); - // we want to seperate players, parse their inventory and compare to request slots - // then ask master for the difference - // we must then prepare it all and inject it into players inventories with lua before - // the next inventory report arrives to avoid duplicating orders - if(inventory && typeof inventory != "string"){ - let players = Object.keys(inventory); - for(let i = 0; i < players.length; i++) { - let pinv = inventory[players[i]]; - // create a copy of player.requestSlots - let toRequest = objectOps.deepclone(pinv.requestSlots); - // subtract items already in inventory - Object.keys(pinv.inventory).forEach(function(key){ - let itemCount = pinv.inventory[key]; - if(toRequest[key]){ - toRequest[key] -= itemCount; - } - let request = { - name:key, - count:toRequest[key], - } - // request the difference from master - needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', request, function (err, response, body) { - if (response && response.body && typeof response.body == "object") { - // confirmed orders, already removed from master inventory - //response.body.name: response.body.count - - } - }); - }); - - } - } - + // process inventory of players and ask master for more stuff + functions.handleInventory(inventory, config); } else if(isJson(chunk) && JSON.parse(chunk).factorioPort){ config = JSON.parse(chunk); + // if its a LUA table message returning with the exports send em off to master + } else if(!isJson(chunk) && !!chunk && config){ + let thing = functions.parseJsString(chunk.replace(/=/g, ":")); + Object.keys(thing).forEach(function(playerName){ + Object.keys(thing[playerName]).forEach(function(itemName){ + needle.post(config.masterIP + ":" + config.masterPort + '/api/place', { + name: itemName, + count: thing[playerName][itemName] + }, function (err, resp, body) { + // console.log(body); + }); + }); + }); } }); process.stdin.on('end', () => { @@ -71,15 +39,6 @@ process.stdin.on('end', () => { // poll inventories setInterval(function(){ - // TODO: Make it work for players other than myself, don't error when inventory does not exist, check logistic request slots - // console.log("/silent-command "+'local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}local d="{inventory:{"for e,f in pairs(a)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;for e,f in pairs(b)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;d=d.."},requestSlots:{"for e,f in pairs(c)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;d=d.."}}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",d)'); - // console.log("Running inventory LUA") - //console.log("/silent-command "+'local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}for d=1,game.players["Danielv123"].force.character_logistic_slot_count do c[d]=game.players["Danielv123"].character.get_request_slot(d)end;local e="{inventory:{"for f,g in pairs(a)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;for f,g in pairs(b)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;e=e.."},requestSlots:{"for f,g in pairs(c)do e=e.."['+"'"+'"..g["name"].."'+"'"+']:"..g["count"]..", "end;e=e.."}}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",e)') - // console.log("/silent-command "+'local a="{"for b,c in pairs(game.players)do local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end;a=a.."}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",a)') - console.log("/silent-command "+'local a="{"for b,c in pairs(game.players)do if c.connected then local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end end;a=a.."}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",a)') - /* - needle.post(config.masterIP+':'+config.masterPort+'/api/editSlaveMeta', {slaveID: config.unique, password: config.clientPassword, meta: {UPS:UPS}}, function(err, resp) { - // success? - }); - */ + console.log("Polling inventories") + console.log(functions.pollInventories(pluginConfig.scriptOutputFileSubscription)); }, 10000); diff --git a/sharedPlugins/inventoryImports/interfacingFunctions.js b/sharedPlugins/inventoryImports/interfacingFunctions.js new file mode 100644 index 00000000..d1bebc22 --- /dev/null +++ b/sharedPlugins/inventoryImports/interfacingFunctions.js @@ -0,0 +1,134 @@ +const objectOps = require("../../lib/objectOps.js"); +const needle = require("needle"); + +// returns JS object, throws if there are syntax errors in input string +function parseJsString(string){ + // eval because my lua is writing a JS object instead of traditional JSON + // this was done because I hate escaping strings and there are more quotes + // in JSON than JS objects. + + // since eval is unsafe, maybe use https://www.npmjs.com/package/eval-sanitizer + if(string.includes("require") || string.includes(";") || string.includes("eval")){ + throw "parseJsString might have gotten something that could be a xss attempt"; + } + let inventory = ""; + eval("inventory = " + string); + console.log(string) + console.log(inventory) + return inventory; + /* Example output: + {'1':{ + inventory: { + 'raw-wood': 4 + }, + requestSlots:{ + 'underground-belt': 50, + 'fast-transport-belt': 50, + 'express-underground-belt': 50 + }}} + */ +} +function handleInventory(inventory, config){ + // we want to seperate players, parse their inventory and compare to request slots + // then ask master for the difference + // we must then prepare it all and inject it into players inventories with lua before + // the next inventory report arrives to avoid duplicating orders + if(inventory && typeof inventory != "string" && typeof inventory == "object"){ + let exports = {}; + let players = Object.keys(inventory); + for(let i = 0; i < players.length; i++) { + let pinv = inventory[players[i]]; + // create a copy of player.requestSlots + let toRequest = objectOps.deepclone(pinv.requestSlots); + // create a place to store the object we have confirmed for importing + let confirmedItems = {}; + let itemsToConfirm = Object.keys(pinv.inventory).length; + + // subtract items already in inventory + Object.keys(pinv.inventory).forEach(function(key){ + let itemCount = pinv.inventory[key]; + if(toRequest[key]){ + toRequest[key] -= itemCount; + } + if(toRequest[key] > 0){ + let request = { + name:key, + count:toRequest[key], + } + // request the difference from master + needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', request, function (err, response, body) { + if (response && response.body && typeof response.body == "object") { + // confirmed orders, already removed from master inventory + //response.body.name: response.body.count + let stack = {name:response.body.name,count:response.body.count} + collectStacks(stack); + } else { + collectStacks(false); + } + }); + } + }); + function collectStacks(stack){ + itemsToConfirm--; + if(stack){ + if(confirmedItems[stack.name]){ + confirmedItems[stack.name] += stack.count; + } else { + confirmedItems[stack.name] = stack.count; + } + } + if(itemsToConfirm <= 0){ + // make lua table + // we gotta construct a string because I can't find any nice library for this + let itemTable = "{"; + Object.keys(confirmedItems).forEach(function(name){ + let count = confirmedItems[name]; + itemTable += '["'+name+'"]='+count+','; + }); + itemTable += "}"; + + console.log('/c local a="'+players[i]+'"local b='+itemTable+'local c={}if game.players[a]and game.players[a].connected then for d,e in pairs(b)do local f=game.players[a].insert{name=d,count=e}if not c[a]then c[a]={}end;if c[a][d]then c[a][d]=c[a][d]+e-f else c[a][d]=e-f end end;game.write_file("'+outputFile+'",serpent.line(c,{["comment"]=false,["compact"]=true}))end'); + } + } + + // handle exports (why would they be there? I don't know!) + if(pinv.exports){ + Object.keys(pinv.exports).forEach(function(name){ + if(exports[name]){ + exports[name] += pinv.exports[name]; + } else { + exports[name] = pinv.exports[name]; + } + }); + } + } + + // keep handling exports here, now we send em off + Object.keys(exports).forEach(function(name){ + needle.post(config.masterIP + ":" + config.masterPort + '/api/place', { + name: name, + count: exports[name] + }, function (err, resp, body) { + // console.log(body); + }); + }); + } else return false; // inventory is falsey or a string +} +function pollInventories(outputFile){ + // console.log("/silent-command "+'local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}local d="{inventory:{"for e,f in pairs(a)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;for e,f in pairs(b)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;d=d.."},requestSlots:{"for e,f in pairs(c)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;d=d.."}}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",d)'); + // console.log("Running inventory LUA") + //console.log("/silent-command "+'local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}for d=1,game.players["Danielv123"].force.character_logistic_slot_count do c[d]=game.players["Danielv123"].character.get_request_slot(d)end;local e="{inventory:{"for f,g in pairs(a)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;for f,g in pairs(b)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;e=e.."},requestSlots:{"for f,g in pairs(c)do e=e.."['+"'"+'"..g["name"].."'+"'"+']:"..g["count"]..", "end;e=e.."}}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",e)') + // console.log("/silent-command "+'local a="{"for b,c in pairs(game.players)do local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end;a=a.."}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",a)') + return "/silent-command "+'local a="{"for b,c in pairs(game.players)do if c.connected then local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end end;a=a.."}"game.write_file("'+outputFile+'",a)' + /* + needle.post(config.masterIP+':'+config.masterPort+'/api/editSlaveMeta', {slaveID: config.unique, password: config.clientPassword, meta: {UPS:UPS}}, function(err, resp) { + // success? + }); + */ +} + +module.exports = { + handleInventory: handleInventory, + parseJsString: parseJsString, + pollInventories: pollInventories, +} \ No newline at end of file diff --git a/sharedPlugins/inventoryImports/interfacingFunctions.spec.js b/sharedPlugins/inventoryImports/interfacingFunctions.spec.js new file mode 100644 index 00000000..c005fe41 --- /dev/null +++ b/sharedPlugins/inventoryImports/interfacingFunctions.spec.js @@ -0,0 +1,61 @@ +const assert = require("assert"); +const child_process = require("child_process"); +const functions = require("./interfacingFunctions"); +const luamin = require("luamin"); + +describe("inventoryImports/interfacingFunctions.js", function(){ + describe("handleInventory(inventory)", function(){ + it("does things to an object", function(){ + let inventory = {'1':{inventory: {"iron-plate":63,'raw-wood': 4},requestSlots:{'raw-wood': 50,'fast-transport-belt': 50,'express-underground-belt': 50}}}; + assert.throws(function(){ + functions.handleInventory(inventory); + }, function(err){ + // convert error to string + let error = ""+err + // expected error is here + /* istanbul ignore else */ + if(error.includes("TypeError: Cannot read property 'masterIP' of undefined")) return true + }, "threw unexpected error"); + }); + it("returns false if given invalid input", function(){ + assert(functions.handleInventory("this is a string") === false, "when given strings it should return false and do nothing"); + }); + }); + describe("pollInventories()", function(){ + it("Returns a big factorio command as string", function(){ + let x = functions.pollInventories("output.txt"); + assert(typeof x == "string", "pollInventories() should always return a string"); + assert(x.includes("/silent-command "), "This is not a command factorio will accept, please prepend /c"); + assert(x.length > 20, "LUA string seems to short, this ain't good"); + }); + it("Returns valid LUA", function(){ + let x = functions.pollInventories("output.txt"); + x = x.replace("/silent-command ","").replace("/c ",""); + let y = false; + assert.doesNotThrow(function(){ + y = luamin.minify(x); + }, "Invalid LUA supplied"); + assert(typeof y == "string", "luamin does not return string???"); + assert(y.length > 20, "Minified LUA too short, something is up"); + }); + }); + describe("parseJsString(string) => object", function(){ + it("converts a string with JS object notation to object", function(){ + let x = functions.parseJsString("{hello:'thisAString!', yes:function(){return 'JS string!'}, not:'json'}"); + assert(!!x == true, "Should under no circumstance return a falsey value, even !!{} is truthy"); + assert(typeof x == "object", "parseJsString should return an object in this case"); + assert(typeof x.yes == "function", "parseJsString is not JSON, so it should be able to contain functions"); + }); + it("*Tries* to avoid XSS attempts by throwing on certain symbols", function(){ + assert.throws(function(){ + let x = functions.parseJsString("{};throw 'fail'"); + }, function(err) { + let error = err+"" + /* istanbul ignore else */ + if(error.includes('parseJsString might have gotten something that could be a xss attempt')){ + return true; + } + }, "should throw when ; are passed because those are the simplest form of xss"); + }); + }) +}); \ No newline at end of file diff --git a/sharedPlugins/inventoryImports/lua commands.lua b/sharedPlugins/inventoryImports/lua commands.lua index 30de641c..d12bee6b 100644 --- a/sharedPlugins/inventoryImports/lua commands.lua +++ b/sharedPlugins/inventoryImports/lua commands.lua @@ -55,19 +55,6 @@ o = o.."}" game.print(o) game.write_file("test.txt", o) --- insert items in players inventory and export leftovers -/c -local name = "Danielv123" -local itemName = "iron-ore" -local itemCount = 100 -if game.players[name] do - local items = {name=itemName, count=itemCount} - if game.players[name].can_insert(items) do - local insertedItems = game.players[name].insert(items) - local restItems = itemCount - insertedItems - end -end - --[[ Minification procedure: @@ -75,14 +62,51 @@ Minification procedure: Run through https://mothereff.in/lua-minifier Replace ' with '+"'"+' Prepend /silent-command +remove game.print(o) +replace "test.txt" with "'+outputFile+'" enclose in '' ]] --- minified strings +-- minified strings (newest on the top) -- offline playes local a="{"for b,c in pairs(game.players)do if c.connected then local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end end;a=a.."}"game.print(a) -- multiple players local a="{"for b,c in pairs(game.players)do local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end;a=a.."}"game.print(a) local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}for d=1,game.players["Danielv123"].force.character_logistic_slot_count do c[d]=game.players["Danielv123"].character.get_request_slot(d)end;local e="{inventory:{"for f,g in pairs(a)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;for f,g in pairs(b)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;e=e.."},requestSlots:{"for f,g in pairs(c)do e=e.."['+"'"+'"..g["name"].."'+"'"+']:"..g["count"]..", "end;e=e.."}}"game.print(e) -local inventory = game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local quickbar = game.players["Danielv123"].get_quickbar().get_contents()local requests = {}for i = 1, game.player.force.character_logistic_slot_count do requests[i] = game.players["Danielv123"].character.get_request_slot(i) end local o = "{inventory:{" for k,v in pairs(inventory) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end for k,v in pairs(quickbar) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end o = o.."},requestSlots:{" for k,v in pairs(requests) do o = o.."['+"'"+'"..v["name"].."'+"'"+']:"..v["count"]..", " end o = o.."}}" game.write_file("test.txt", o) \ No newline at end of file +local inventory = game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local quickbar = game.players["Danielv123"].get_quickbar().get_contents()local requests = {}for i = 1, game.player.force.character_logistic_slot_count do requests[i] = game.players["Danielv123"].character.get_request_slot(i) end local o = "{inventory:{" for k,v in pairs(inventory) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end for k,v in pairs(quickbar) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end o = o.."},requestSlots:{" for k,v in pairs(requests) do o = o.."['+"'"+'"..v["name"].."'+"'"+']:"..v["count"]..", " end o = o.."}}" game.write_file("test.txt", o) + + +-- insert items in players inventory and export leftovers +-- name should be players[i] + +/c +local name = "Danielv123" +local items = {["iron-ore"]=30, ["steel-plate"]=120} +local items_failed = {} +if(game.players[name] and game.players[name].connected) then + for item,count in pairs(items) do + local i = game.players[name].insert{name=item,count=count} + if not items_failed[name] then + items_failed[name] = {} + end + if items_failed[name][item] then + items_failed[name][item] = items_failed[name][item] + count - i + else + items_failed[name][item] = count - i + end + end + game.write_file("t.txt", serpent.line(items_failed, {["comment"]=false, ["compact"]=true})) +end + +--[[ +Minification procedure: + +Run through https://mothereff.in/lua-minifier +Prepend /silent-command +replace "t.txt" with "'+outputFile+'" +enclose in '' +]] + +local a="Danielv123"local b={["iron-ore"]=30,["steel-plate"]=120}local c={}if game.players[a]and game.players[a].connected then for d,e in pairs(b)do local f=game.players[a].insert{name=d,count=e}if not c[a]then c[a]={}end;if c[a][d]then c[a][d]=c[a][d]+e-f else c[a][d]=e-f end end;game.write_file("'+outputFile+'",serpent.line(c,{["comment"]=false,["compact"]=true}))end +