From 4961908376db687a74ff4a6b134430f1a57ce57b Mon Sep 17 00:00:00 2001 From: "Riley L." Date: Sun, 3 Nov 2024 18:31:13 +0100 Subject: css and rebrad --- .gitignore | 2 +- abrechenbarkeit.lua | 400 ++++++++++++++++++++++++++++++++++++++++++++++++++++ gnix.yaml | 2 +- readme.md | 4 +- strichliste.lua | 393 --------------------------------------------------- 5 files changed, 404 insertions(+), 397 deletions(-) create mode 100755 abrechenbarkeit.lua delete mode 100755 strichliste.lua diff --git a/.gitignore b/.gitignore index 3564e40..ffe2fa3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /* !/gnix.yaml !/readme.md -!/strichliste.lua +!/abrechenbarkeit.lua !/.gitignore !/collapse_log.lua diff --git a/abrechenbarkeit.lua b/abrechenbarkeit.lua new file mode 100755 index 0000000..dc3685b --- /dev/null +++ b/abrechenbarkeit.lua @@ -0,0 +1,400 @@ +#!/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("a.css"):read("a") +-- local stylesheet = [[ +-- /* body { background-color: #161616; } +-- h1, h2, h3, h4, h5, h6, p, label, a { color: #e2e2e2; } */ +-- .amount-presets form { display: inline-block; width: 60px } +-- .amount-pos { color: green; } +-- .amount-neg { color: red; } +-- nav h2 { display: inline-block } +-- .notif { padding: 0.5em; margin: 0.5em; background-color: #ddd; } +-- .notif.error { background-color: #faa; } +-- .notif p { margin: 5px; } +-- form.box { border: 2px solid grey; padding: 0.5em; margin: 0.5em; display: inline-block; } +-- form h3 { margin: 5px; } +-- ]] + +local script = [[ + document.addEventListener("keypress", ev => { + if (!(document.activeElement instanceof HTMLInputElement)) { + if (ev.code.startsWith("Digit")) + document.forms.buy_product.product.value += ev.code.substring(5) + if (ev.code == "Enter") + document.forms.buy_product.submit() + } + }) +]] + +local function respond(status, title, body) + print(string.format("Status: %d", status)) + print("Content-Type: text/html") + print("") + print(string.format([[ + + + %s + + + + %s + + + + ]], escape(title), stylesheet, script, config.head_extra or "")) + body() + print("") +end + +local function respond_error(message) + respond(400, "Error", function() + print(string.format("

Error: %s

", escape(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, comment = string.match(l, "(%d+),([%w_ -]+),(-?%d+),([%w_ -]*)") + return tonumber(time), username, tonumber(amount), 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, amount, name = string.match(l, "([%w_-]+),(-?%d+),([%w_ -]*)") + return barcode, tonumber(amount), name + 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 last_txns() + local users = {} + for time, username, _, _ in read_log() do + users[username] = time + end + return users +end + +local function error_box(message) + return string.format([[

Error: %s

]], message) +end + +local function r_user_post(username) + local data = form_data() + local amount = nil + local comment = "" + if data.product then + for p_barcode, p_amount, p_name in read_products() do + if p_barcode == data.product then + amount = p_amount + comment = p_name + end + end + if amount == nil then + return error_box("unknown product") + end + else + amount = tonumber(data.amount) + comment = data.comment or "" + end + if amount == nil then + return error_box("amount invalid") + end + if 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() + log:write(string.format("%d,%s,%d,%s\n", time, username, amount, comment)) + 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(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)) + 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

+ + + +
+ ]]) + print("
") + end) +end + +local function r_log(filter) + return respond(200, "Abrechnungen", function() + print("") + print("") + for time, username, amount, comment in read_log() do + if filter == nil or filter == username then + print(string.format([[ + + + + + + + + ]], + time, format_duration(os.time() - time), + escape(username), + amount >= 0 and "pos" or "neg", amount / 100, + escape(comment), + escape(username), + -amount, + escape(comment) + )) + end + end + print("
TimeUsernameAmountComment
%d (%s ago)%s%.02f€%s +
+ + + +
+
") + end) +end + +local function r_index() + return respond(200, "Abrechenbarkeit", function() + print([[ +
+

User Creation

+ + + +
+ ]]) + print("") + end) +end + +local function r_create_user() + local username = query.create_user + if username:match("^([%w_ -]+)$") == nil then + return respond_error("invalid username " .. username) + end + return redirect(string.format("/%s", urlencode(username))) +end + +local function r_products() + respond(200, "Abrechenbare Products", function() + print("

Product List

") + print("") + for barcode, price, name in read_products() do + print(string.format([[ + + ]], + name, + price >= 0 and "pos" or "neg", price / 100, + barcode + )) + end + print("
NamePriceBarcode
%s%.02f€%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 then + return respond_error("username invalid") + elseif query.log then + return r_log(username) + else + return r_user(username) + end +end diff --git a/gnix.yaml b/gnix.yaml index 5f8470c..8815f2a 100644 --- a/gnix.yaml +++ b/gnix.yaml @@ -1,2 +1,2 @@ http: { bind: "0.0.0.0:8080" } -handler: !cgi { bin: ./strichliste.lua } +handler: !cgi { bin: ./abrechenbarkeit.lua } diff --git a/readme.md b/readme.md index 2ea03f7..4785d95 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,8 @@ -# Strichliste v2 +# Abrechenbarkeit v2 A _simpler_ trust based ledger. -The entire application is contained within `strichliste.lua`. This script +The entire application is contained within `abrechenbarkeit.lua`. This script implements CGI. It was tested against Lua version 5.4.7. Application data is stored in a number of files in the process working directory (See below). diff --git a/strichliste.lua b/strichliste.lua deleted file mode 100755 index fbdfbb5..0000000 --- a/strichliste.lua +++ /dev/null @@ -1,393 +0,0 @@ -#!/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 = [[ - /* body { background-color: #161616; } - h1, h2, h3, h4, h5, h6, p, label, a { color: #e2e2e2; } */ - .amount-presets form { display: inline-block; width: 60px } - .amount-pos { color: green; } - .amount-neg { color: red; } - nav h2 { display: inline-block } - .notif { padding: 0.5em; margin: 0.5em; background-color: #ddd; } - .notif.error { background-color: #faa; } - .notif p { margin: 5px; } - form.box { border: 2px solid grey; padding: 0.5em; margin: 0.5em; display: inline-block; } - form h3 { margin: 5px; } -]] - -local script = [[ - document.addEventListener("keypress", ev => { - if (!(document.activeElement instanceof HTMLInputElement)) { - if (ev.code.startsWith("Digit")) - document.forms.buy_product.product.value += ev.code.substring(5) - if (ev.code == "Enter") - document.forms.buy_product.submit() - } - }) -]] - -local function respond(status, title, body) - print(string.format("Status: %d", status)) - print("Content-Type: text/html") - print("") - print(string.format([[ - - - %s - - - - %s - - - - ]], escape(title), stylesheet, script, config.head_extra or "")) - body() - print("") -end - -local function respond_error(message) - respond(400, "Error", function() - print(string.format("

Error: %s

", escape(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, comment = string.match(l, "(%d+),([%w_ -]+),(-?%d+),([%w_ -]*)") - return tonumber(time), username, tonumber(amount), 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, amount, name = string.match(l, "([%w_-]+),(-?%d+),([%w_ -]*)") - return barcode, tonumber(amount), name - 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 last_txns() - local users = {} - for time, username, _, _ in read_log() do - users[username] = time - end - return users -end - -local function error_box(message) - return string.format([[

Error: %s

]], message) -end - -local function r_user_post(username) - local data = form_data() - local amount = nil - local comment = "" - if data.product then - for p_barcode, p_amount, p_name in read_products() do - if p_barcode == data.product then - amount = p_amount - comment = p_name - end - end - if amount == nil then - return error_box("unknown product") - end - else - amount = tonumber(data.amount) - comment = data.comment or "" - end - if amount == nil then - return error_box("amount invalid") - end - if 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() - log:write(string.format("%d,%s,%d,%s\n", time, username, amount, comment)) - 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("Strichliste: %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(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)) - end - print([[ -

-

Create Transaction

- -
- -
- -
-
-

Buy Product

- -
- -
- ]]) - 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 - print("
") - end - print("
") - end) -end - -local function r_log(filter) - return respond(200, "Strichliste Log", function() - print("") - print("") - for time, username, amount, comment in read_log() do - if filter == nil or filter == username then - print(string.format([[ - - - - - - - - ]], - time, format_duration(os.time() - time), - escape(username), - amount >= 0 and "pos" or "neg", amount / 100, - escape(comment), - escape(username), - -amount, - escape(comment) - )) - end - end - print("
TimeUsernameAmountComment
%d (%s ago)%s%.02f€%s -
- - - -
-
") - end) -end - -local function r_index() - return respond(200, "Strichliste", function() - print([[ -
-

User Creation

- -
- -
- ]]) - print("") - end) -end - -local function r_create_user() - local username = query.create_user - if username:match("^([%w_ -]+)$") == nil then - return respond_error("invalid username " .. username) - end - return redirect(string.format("/%s", urlencode(username))) -end - -local function r_products() - respond(200, "Strichliste Product List", function() - print("

Product List

") - print("") - for barcode, price, name in read_products() do - print(string.format([[ - - ]], - name, - price >= 0 and "pos" or "neg", price / 100, - barcode - )) - end - print("
NamePriceAmount
%s%.02f€%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 then - return respond_error("username invalid") - elseif query.log then - return r_log(username) - else - return r_user(username) - end -end -- cgit v1.2.3-70-g09d2