#!/usr/bin/env luajit local function escape(s) return s:gsub("<", "<"):gsub("<", "<") end local function urldecode(s) if s == nil then return nil end return s:gsub("+", " "):gsub("%%20", " ") end local function urlencode(s) if s == nil then return nil end return s:gsub(" ", "%%20") end local function parse_query(q) if q == nil then return {} end local data = {} for pair in string.gmatch(q, "([^&]+)") do local flag = string.match(pair, "^([^=]+)$") if flag ~= nil then data[flag] = "1" else local key, value = string.match(pair, "^([^=]+)=([^=]*)$") if key ~= nil and value ~= nil then data[key] = urldecode(value) end end end return data end local function load_config() local log = io.open("config", "r") if log == nil then return {} end local config = {} for l in log:lines("l") do if l ~= "" and l[0] ~= "#" then local key, value = string.match(l, "^([^=]+)=([^=]*)") if key ~= nil and value ~= nil then config[key] = value end end end return config end local config = load_config() local path = os.getenv("PATH_INFO") local method = os.getenv("REQUEST_METHOD") local query = parse_query(os.getenv("QUERY_STRING")) local stylesheet = io.open("style.css"):read("a") local script = io.open("script.js"):read("a") local function get_user_theme(username) local c = "" local js = "" if username == "_jeb" then c = "html { animation: 2s jeb infinite; }" c = c .. "@keyframes jeb {\n" for i=0,100 do c = c .. string.format("%.02f%% { --hue: %.02f; } \n", i, i / 100 * 360) end c = c .. "\n}" elseif username == "Dinnerbone" then c = "html { transform: scale(-1); } " end return c, js end local function respond(status, title, body) local themecss, themejs = get_user_theme(path and path:sub(2)) print(string.format("Status: %d", status)) print("Content-Type: text/html") print("") print(string.format([[ %s %s ]], escape(title), stylesheet, -- style.css themecss, -- theme for user script, -- script.js config.head_extra or "" )) body() print(string.format( "", themejs )) end local function error_box(message) return string.format([[

Error: %s

]], message) end local function respond_error(message) respond(400, "Error", function() print(error_box(message)) end) end local function redirect(path) print("Status: 307") print(string.format("Location: %s", path)) print() end local function form_data() return parse_query(io.read()) end local function format_duration(t) if t > 86400 then return string.format("%d day%s", t / 86400, math.floor(t / 86400) ~= 1 and "s" or "") end if t > 3600 then return string.format("%d hour%s", t / 3600, math.floor(t / 3600) ~= 1 and "s" or "") end if t > 60 then return string.format("%d minute%s", t / 60, math.floor(t / 60) ~= 1 and "s" or "") end return string.format("%d seconds", t) end local function read_log() local log = io.open("log", "r") if log == nil then return function() return nil end end local lines = log:lines("l") return function() local l = lines() if l == "" or l == nil then return nil end local time, username, amount, pcode, pcount, comment = string.match(l, "(%d+),([%w_ -]+),(-?%d+),([%w_-]*),(-?%d*),([%w_ -]*)") return tonumber(time), username, tonumber(amount), pcode, tonumber(pcount), comment end end local function read_products() local log = io.open("products", "r") if log == nil then return function() return nil end end local lines = log:lines("l") return function() local l = lines() if l == "" or l == nil then return nil end local barcode, price, name, owner = string.match(l, "([%w_-]+),(-?%d+),([%w_ -]*),([%w_ -]*)") return barcode, tonumber(price), name, owner end end local function balances() local users = {} for _, username, amount, _, _, _ in read_log() do users[username] = (users[username] or 0) + amount end return users end local function product_balances() local products = {} for _, _, _, pcode, pcount, _ in read_log() do if pcode ~= nil and pcount ~= nil then products[pcode] = (products[pcode] or 0) + pcount end end return products end local function last_txns() local users = {} for time, username, _, _, _, _ in read_log() do users[username] = time end return users end local function get_active_users() local user_balances = {} for time, username, amount, _, _, _ in read_log() do user_balances[username] = { time = time, username = username, balance = (user_balances[username] or { balance = 0 }).balance + amount } end local users = {} for _, user in pairs(user_balances) do table.insert(users, user) end table.sort(users, function(a, b) return a.time > b.time end) return users end local function r_user_post(username) local data = form_data() local amount = tonumber(data.amount) local comment = data.comment local pcode = nil local pcount = nil local powner = nil if data.pcode then for p_barcode, p_amount, p_name, p_owner in read_products() do if p_barcode == data.pcode then powner = p_owner pcount = (tonumber(data.pcount) or 1) * (data.negate_pcount ~= nil and -1 or 1) pcode = p_barcode if amount == nil then amount = pcount * p_amount end if comment == nil then comment = string.format("%s %d %s", pcount < 0 and "Buy" or "Restock", math.abs(pcount), p_name) powner_comment = string.format("%s %d %s %s %s", pcount < 0 and "Sell" or "Restock", math.abs(pcount), p_name, pcount < 0 and "to" or "by", username) end end end if amount == nil then return error_box("unknown product") end end if amount == nil then return error_box("amount invalid") end if comment == nil or comment:match("^[%w_ -]*$") == nil then return error_box("comment invalid") end local log = io.open("log", "a+") if log == nil then return error_box("failed to open log") end local time = os.time() -- subtract from buyer log:write(string.format("%d,%s,%d,%s,%s,%s\n", time, username, amount, pcode or "", pcount or "", comment)) -- add to owner if powner then -- count is always zero as doesn't affect stock log:write(string.format("%d,%s,%d,%s,%s,%s\n", time, powner, -amount, pcode or "", "", powner_comment)) end log:flush() log:close() return string.format([[

Transaction successful: %.02f€ (%s)

]], amount >= 0 and "pos" or "neg", amount / 100, escape(comment), config.transaction_sound or "" ) end local function r_user(username) local notif = nil if method == "POST" then notif = r_user_post(username) end return respond(200, string.format("Abrechenbarheit: %s", username), function() print(string.format("

%s

", username)) local balance = balances()[username] local last_txn = last_txns()[username] local new_user = balance == nil balance = balance or 0 if notif then print(notif) end if new_user then print([[

This user account does not exist yet. It will only be created after the first transaction.

]]) else print([[
]]) print(string.format([[ Current balance:
%.02f€
]], balance >= 0 and "pos" or "neg", balance / 100)) print(string.format([[ Last transaction added %s ago. View user log ]], format_duration(os.time() - last_txn), username)) print([[
]]) end print([[
]]) print([[
]]) for _, type in ipairs({ 1, -1 }) do for _, amount in ipairs({ 50, 100, 150, 200, 500, 1000 }) do print(string.format([[
]], amount * type, ({ [-1] = "-", [1] = "+" })[type], amount / 100, ({ [-1] = "neg", [1] = "pos" })[type])) end end print("
") print([[

Create Transaction

Buy Product

Restock Product

]]) print("
") end) end local function r_log(filter) return respond(200, "Abrechnungen", function() print([[]]) print("") for time, username, amount, pcode, pcount, comment in read_log() do if filter == nil or filter == username then print(string.format([[ ]], os.date("!%Y-%m-%dT%H:%M:%SZ", time), format_duration(os.time() - time), escape(username), amount >= 0 and "pos" or "neg", amount / 100, escape(pcode) or "", pcount and (pcount < 0 and "buy" or "stock") or "", pcount and tostring(math.abs(pcount)) or "", escape(comment), escape(username), -amount, escape(comment) )) end end print("") print("
Time Username Amount P.-Barcode P.-Count Comment Actions
%s (%s ago) %s %.02f€ %s %s %s %s
") end) end local function r_index() return respond(200, "Abrechenbarkeit", function() print([[

User Creation

]]) print([[
]]) -- for printing print([[") end) end local function validate_username(username) -- disallow leading or traling whitespace return username ~= nil and username:match("^([%w_ -]+)$") ~= nil and username:match("^%s") == nil and username:match("%s$") == nil end local function r_create_user() local username = query.create_user if not validate_username(username) then return respond_error("invalid username " .. username) end return redirect(string.format("/%s", urlencode(username))) end local function r_products_post() local data = form_data() local barcode = data.barcode if barcode == nil then return error_box("barcode unset") end if barcode:match("^[%w_-]*$") == nil then return error_box("barcode invalid") end if data.delete then local new_products = io.open("products.new", "w+") if new_products == nil then return error_box("failed to open new products") end for a_barcode, price, name, owner in read_products() do if barcode ~= a_barcode then new_products:write(string.format("%s,%d,%s,%s\n", a_barcode, price, name, owner)) end end new_products:flush() new_products:close() os.rename("products.new", "products") else local price = tonumber(data.price) local name = data.name if price == nil then return error_box("price invalid") end if name:match("^[%w_ -]*$") == nil then return error_box("name invalid") end local products = io.open("products", "a+") if products == nil then return error_box("failed to open products") end local owner = data.owner or "" if name:match("^[%w_ -]*$") == nil then return error_box("owner invalid") end products:write(string.format("%s,%d,%s,%s\n", barcode, price, name, owner)) products:flush() products:close() end end local function r_products() local notif = nil if method == "POST" then notif = r_products_post() end respond(200, "Abrechenbare Product List", function() print("

Product List

") if notif then print(notif) end print([[

Add Product

Remove Product

]]) print([[]]) local pbals = product_balances() for barcode, price, name, owner in read_products() do print(string.format([[ ]], name, -price >= 0 and "pos" or "neg", -price / 100, barcode, pbals[barcode] or "0", owner )) end print("
NamePriceBarcodeCountOwner
%s%.02f€%s%s%s
") end) end local function extract_username() if path == nil then return respond_error("no path") end local username = urldecode(path:sub(2)) if username == nil or username:match("^([%w_ -]+)$") == nil then return nil end return username end if path == "/" then if query.products then return r_products() elseif query.log then return r_log() elseif query.create_user then return r_create_user() else return r_index() end else local username = extract_username() if username == nil or not validate_username(username) then return respond_error("username invalid") elseif query.log then return r_log(username) else return r_user(username) end end