Initial commit
This commit is contained in:
commit
14602222e7
3 changed files with 603 additions and 0 deletions
121
LICENSE
Normal file
121
LICENSE
Normal file
|
@ -0,0 +1,121 @@
|
|||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
29
README.md
Normal file
29
README.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# c4mate - command line client for the C4 mukas instance (aka Mate-Pad)
|
||||
|
||||
## Requirements
|
||||
|
||||
* lua
|
||||
* curl
|
||||
* sed
|
||||
* date
|
||||
|
||||
## Usage
|
||||
|
||||
c4mate [-u <user>] [ <query> | -b <id> | -l | +/-<amount> | -g <amount> <user> [<reason>]]
|
||||
Display current balance:
|
||||
c4mate
|
||||
Find and buy item (interactive):
|
||||
c4mate <query>
|
||||
Buy by item id (non-interactive):
|
||||
c4mate -b|--buy <id>
|
||||
Show log:
|
||||
c4mate -l|--log
|
||||
Add or subtract credits:
|
||||
c4mate <difference>
|
||||
Give credits:
|
||||
c4mate -g <amount> <user> [<reason>]
|
||||
|
||||
## License
|
||||
|
||||
CC0
|
||||
|
453
c4mate
Executable file
453
c4mate
Executable file
|
@ -0,0 +1,453 @@
|
|||
#!/bin/lua
|
||||
--
|
||||
-- c4mate: command line client for the C4 mukas instance (aka Mate-Pad).
|
||||
--
|
||||
-- Author: Shy
|
||||
-- License: CC0
|
||||
|
||||
-- Enter your user name here:
|
||||
USER = nil
|
||||
HOST = "http://mate.labor.koeln.ccc.de"
|
||||
|
||||
function utf8_decode(str)
|
||||
-- Replace JSON unicode escape sequences with chars.
|
||||
if str == nil then return "" end
|
||||
return string.gsub(str, "\\u(%x%x%x%x)",
|
||||
function (s) return utf8.char(tonumber(s, 16)) end)
|
||||
end
|
||||
|
||||
function url_encode(str)
|
||||
-- URL-encode string. Used only for usernames.
|
||||
-- Todo: make this more sophisticated.
|
||||
return string.gsub(str, " ", "%%20")
|
||||
end
|
||||
|
||||
MatePad = {
|
||||
CURL_FLAGS = "--silent --show-error --fail --user-agent 'c4mate' --max-redirs 0 --max-filesize 65536",
|
||||
}
|
||||
function MatePad:new(HOST, USER)
|
||||
local o = {
|
||||
HOST = HOST,
|
||||
USER = USER,
|
||||
COOKIE_JAR,
|
||||
balance
|
||||
}
|
||||
-- Metatable. Make garbage collector delete our temporary cookie jar.
|
||||
local m = {
|
||||
__index = self,
|
||||
__gc = function (f)
|
||||
if f.COOKIE_JAR ~= nil then os.remove(f.COOKIE_JAR) end
|
||||
end
|
||||
}
|
||||
return setmetatable(o, m)
|
||||
end
|
||||
|
||||
-- Fetch response from given api endpoint.
|
||||
function MatePad:fetch(api, data)
|
||||
local responce, curl, ok, exit, signal
|
||||
-- Set up cookie jar if not yet present.
|
||||
if self.COOKIE_JAR == nil then self.COOKIE_JAR = os.tmpname() end
|
||||
|
||||
if data ~= nil then
|
||||
local flags = ""
|
||||
for k, v in pairs(data) do
|
||||
flags = string.format("%s --data-urlencode \"%s=%s\"", flags, k, v)
|
||||
end
|
||||
curl = io.popen(string.format(
|
||||
"curl %s --cookie \"%s\" --cookie-jar \"%s\" %s \"%s/%s\"",
|
||||
self.CURL_FLAGS, self.COOKIE_JAR, self.COOKIE_JAR, flags, self.HOST, api))
|
||||
else
|
||||
curl = io.popen(string.format(
|
||||
"curl %s --cookie \"%s\" --cookie-jar \"%s\" \"%s/%s\"",
|
||||
self.CURL_FLAGS, self.COOKIE_JAR, self.COOKIE_JAR, self.HOST, api))
|
||||
end
|
||||
responce = curl:read("a")
|
||||
|
||||
ok, exit, signal = curl:close()
|
||||
if not ok then
|
||||
if exit == "signal" then
|
||||
io.stderr:write("Error: 'curl' received signal "..signal..".\n")
|
||||
else
|
||||
io.stderr:write("Error: 'curl' returned exit code "..signal..". ")
|
||||
if signal == 22 then
|
||||
io.stderr:write("Maybe incorrect username?\n")
|
||||
else
|
||||
io.stderr:write("\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
return responce
|
||||
end
|
||||
|
||||
-- Initialize session.
|
||||
function MatePad:init()
|
||||
self.token = self:fetch("api/csrf_token")
|
||||
-- Todo: check token.
|
||||
end
|
||||
|
||||
-- Update user balance.
|
||||
function MatePad:update_balance()
|
||||
self.balance = self:fetch("api/user/"..url_encode(self.USER).."/balance?_csrf_token="..self.token)
|
||||
return self.balance ~= ""
|
||||
end
|
||||
|
||||
-- Parse history.
|
||||
function MatePad:parse_log()
|
||||
local pipe, ok, exit, signal
|
||||
local datefile = os.tmpname()
|
||||
local log = {}
|
||||
-- sed #1: Insert newlines between log entries.
|
||||
-- sed #2: Extract time and write it into datefile. Then extract and print
|
||||
-- oldbalance, newbalance, parameter and method. Depending on method, print
|
||||
-- either reason or item_name. If any extraction fails we fail with exit
|
||||
-- code 1.
|
||||
pipe = io.popen(string.format([=[
|
||||
curl %s --header "Accept: application/json" "%s/api/user/%s/log?type=json" | \
|
||||
sed -n --sandbox '
|
||||
/.*\[\([^]]*\)\].*/{
|
||||
s//\1/
|
||||
s/} *,/&\n/gp
|
||||
}' | \
|
||||
sed -n '
|
||||
h
|
||||
s/.*"time" *: *"\([^"]\+\)".*/\1UTC/
|
||||
T fail
|
||||
w %s
|
||||
g
|
||||
s/.*"oldbalance" *: *\(-\?[0-9]\+\).*/\1/p
|
||||
T fail
|
||||
g
|
||||
s/.*"newbalance" *: *\(-\?[0-9]\+\).*/\1/p
|
||||
T fail
|
||||
g
|
||||
s/.*"parameter" *: *\([0-9]\+\).*/\1/p
|
||||
T fail
|
||||
g
|
||||
s/.*"method" *: *"\([^"]\+\)".*/\1/p
|
||||
T fail
|
||||
/set_balance\|transfer/{
|
||||
g
|
||||
s/.*"reason" *: *\(null\|"\([^"]*\)"\).*/\2/p
|
||||
T fail
|
||||
b
|
||||
}
|
||||
g
|
||||
s/.*"item_name" *: *\(null\|"\([^"]*\)"\).*/\2/p
|
||||
T fail
|
||||
b
|
||||
:fail
|
||||
q 1
|
||||
']=], self.CURL_FLAGS, self.HOST, url_encode(self.USER), datefile))
|
||||
|
||||
while true do
|
||||
local entry = {}
|
||||
local oldbalance = tonumber(pipe:read("l"))
|
||||
if oldbalance == nil then break end
|
||||
entry.oldbalance = oldbalance
|
||||
entry.newbalance = tonumber(pipe:read("l"))
|
||||
entry.parameter = tonumber(pipe:read("l"))
|
||||
entry.method = pipe:read("l")
|
||||
if entry.method == "buy" or entry.method == "recharge" then
|
||||
entry.name = utf8_decode(pipe:read("l"))
|
||||
else
|
||||
-- set_balance, transferTo, transferFrom
|
||||
entry.reason = utf8_decode(pipe:read("l"))
|
||||
end
|
||||
table.insert(log, entry)
|
||||
end
|
||||
|
||||
-- Check pipe exit status.
|
||||
ok, exit, signal = pipe:close()
|
||||
if not ok then -- sed error
|
||||
io.stderr:write("Sorry, there was an error while parsing the API response.\n")
|
||||
-- An empty log will throw no further exceptions.
|
||||
log = {}
|
||||
elseif log == {} then -- curl error
|
||||
io.stderr:write("Sorry, there was an error while connecting the API.\n")
|
||||
end
|
||||
|
||||
-- Convert timestamps to local time.
|
||||
local timestamps = io.popen("date -f "..datefile.." +'%_c'")
|
||||
for i = 1, #log do
|
||||
log[i].time = timestamps:read("l")
|
||||
end
|
||||
|
||||
-- Check date exit code.
|
||||
ok, exit, signal = timestamps:close()
|
||||
os.remove(datefile)
|
||||
if not ok then
|
||||
io.stderr:write(string.format(
|
||||
"Warning: 'date' exited with non-zero exit code (%s/%s).\n",
|
||||
exit, signal))
|
||||
end
|
||||
return log
|
||||
end
|
||||
|
||||
-- Print given history.
|
||||
function MatePad:print_log(log)
|
||||
local event
|
||||
for i = #log, 1, -1 do
|
||||
event = log[i]
|
||||
if event.method == "buy" or event.method == "recharge" then
|
||||
print(string.format(
|
||||
"[%s] Bought %s",
|
||||
event.time, event.name
|
||||
))
|
||||
elseif event.method == "set_balance" then
|
||||
print(string.format(
|
||||
"[%s] Set balance from %.2f € to %.2f €",
|
||||
event.time, event.oldbalance/100, event.newbalance/100
|
||||
))
|
||||
elseif event.method == "transferTo" then
|
||||
print(string.format(
|
||||
"[%s] Transferred %.2f € to #%d (%s)",
|
||||
event.time, (event.oldbalance - event.newbalance)/100,
|
||||
event.parameter, event.reason
|
||||
))
|
||||
elseif event.method == "transferFrom" then
|
||||
print(string.format(
|
||||
"[%s] Received %.2f € from #%d (%s)",
|
||||
event.time, (event.newbalance - event.oldbalance)/100,
|
||||
event.parameter, event.reason
|
||||
))
|
||||
else
|
||||
print("Warning: unknown event \""..event.method.."\".")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Get availably items.
|
||||
function MatePad:get_roster()
|
||||
local pipe, ok, exit, signal
|
||||
local roster = {}
|
||||
-- sed #1: Extract items section and insert newlines between entries.
|
||||
-- sed #2: Extract and print id, name and price. If any extraction fails we
|
||||
-- fail with exit code 1.
|
||||
pipe = io.popen(string.format([=[
|
||||
curl %s --header "Accept: application/json" "%s/api/items" | \
|
||||
sed -n --sandbox '
|
||||
/.*"items": *\[\( *{.*}\)*\].*/{
|
||||
s//\1/
|
||||
s/} *,/&\n/gp
|
||||
}' | \
|
||||
sed -n --sandbox '
|
||||
h
|
||||
s/.*"id" *: *\([0-9]\+\).*/\1/p
|
||||
T fail
|
||||
g
|
||||
s/.*"name" *: *"\([^"]*\)".*/\1/p
|
||||
T fail
|
||||
g
|
||||
s/.*"price" *: *\(-\?[0-9]\+\).*/\1/p
|
||||
T fail
|
||||
b
|
||||
:fail
|
||||
q 1
|
||||
']=], self.CURL_FLAGS, self.HOST))
|
||||
|
||||
while true do
|
||||
local item = {}
|
||||
item.id = tonumber(pipe:read("l"))
|
||||
if item.id == nil then break end
|
||||
item.name = utf8_decode(pipe:read("l"))
|
||||
item.price = tonumber(pipe:read("l"))
|
||||
table.insert(roster, item)
|
||||
end
|
||||
|
||||
ok, exit, signal = pipe:close()
|
||||
if not ok then -- sed error
|
||||
io.stderr:write("Sorry, there was an error while parsing the API response.\n")
|
||||
-- Empty table will throw no exceptions.
|
||||
return {}
|
||||
elseif roster == nil then -- curl error
|
||||
io.stderr:write("Sorry, there was an error while connecting the API.\n")
|
||||
return {}
|
||||
end
|
||||
return roster
|
||||
end
|
||||
|
||||
-- Buy given item.
|
||||
function MatePad:buy(id)
|
||||
return self:fetch(string.format(
|
||||
"api/user/%s/buy/%d?_csrf_token=%s",
|
||||
url_encode(self.USER), id, self.token))
|
||||
end
|
||||
|
||||
-- Set new balance (in cents).
|
||||
function MatePad:set_balance(amount)
|
||||
return self:fetch(string.format(
|
||||
"api/user/%s/balance?_csrf_token=%s",
|
||||
url_encode(self.USER), self.token),
|
||||
{newbalance = amount})
|
||||
end
|
||||
|
||||
-- Transfer credits to given user.
|
||||
function MatePad:give(amount, recipient, reason)
|
||||
return self:fetch(string.format(
|
||||
"api/user/%s/transfer?_csrf_token=%s",
|
||||
url_encode(self.USER), self.token),
|
||||
{
|
||||
recipient = recipient,
|
||||
amount = amount,
|
||||
reason = reason or ""
|
||||
})
|
||||
end
|
||||
|
||||
-- Process help and username options.
|
||||
if arg[1] == "-h" or
|
||||
arg[1] == "--help" then
|
||||
-- Usage.
|
||||
print([=[
|
||||
Usage: c4mate [-u <user>] [ <query> | -b <id> | -l | +/-<amount> | -g <amount> <user> [<reason>]]
|
||||
Display current balance:
|
||||
c4mate
|
||||
Find and buy item (interactive):
|
||||
c4mate <query>
|
||||
Buy by item id (non-interactive):
|
||||
c4mate -b|--buy <id>
|
||||
Show log:
|
||||
c4mate -l|--log
|
||||
Add or subtract credits:
|
||||
c4mate <difference>
|
||||
Give credits:
|
||||
c4mate -g <amount> <user> [<reason>]]=])
|
||||
return
|
||||
|
||||
-- Parse -u flag.
|
||||
elseif arg[1] == "-u" then
|
||||
table.remove(arg, 1)
|
||||
USER = arg[1]
|
||||
table.remove(arg, 1)
|
||||
end
|
||||
|
||||
-- Set up username.
|
||||
if USER == nil then
|
||||
USER = os.getenv("USER")
|
||||
if USER == nil then
|
||||
io.stderr:write("Error: no username given and $USER environment variable empty!\n")
|
||||
os.exit(1)
|
||||
end
|
||||
end
|
||||
pad = MatePad:new(HOST, USER)
|
||||
|
||||
if arg[1] == nil then
|
||||
-- Print balance.
|
||||
pad:init()
|
||||
if pad:update_balance() then
|
||||
print(string.format(
|
||||
"Balance of user %s: %.2f €", pad.USER, pad.balance/100))
|
||||
end
|
||||
|
||||
elseif arg[1] == "-l" or
|
||||
arg[1] == "--log" then
|
||||
-- Print log.
|
||||
pad:print_log(pad:parse_log())
|
||||
|
||||
elseif arg[1] == "-g" or
|
||||
arg[1] == "--give" then
|
||||
-- Give.
|
||||
local amount = tonumber(arg[2])
|
||||
local recipient = arg[3]
|
||||
local reason = table.concat(arg, " ", 4)
|
||||
if amount == nil or recipient == nil then
|
||||
print("Error: could not parse amount and/or recipient.")
|
||||
print("Usage: c4mate -g <amount> <user> [<reason>]")
|
||||
return
|
||||
end
|
||||
|
||||
pad:init()
|
||||
local ok = pad:give(amount, recipient, reason)
|
||||
|
||||
if ok == "OK" then
|
||||
pad:update_balance()
|
||||
print(string.format(
|
||||
"%.2f € transferred from %s to %s.\nNew balance of user %s: %.2f €",
|
||||
amount, pad.USER, recipient, pad.USER, pad.balance/100))
|
||||
else
|
||||
print("Sorry, there was an error. ("..ok..")")
|
||||
end
|
||||
|
||||
elseif arg[1] == "-b" or arg[1] == "--buy" then
|
||||
local id = tonumber(arg[2])
|
||||
if id == nil then
|
||||
print("Usage: c4mate --buy <id>")
|
||||
return
|
||||
end
|
||||
|
||||
pad:init()
|
||||
if pad:buy(id) == "OK" then
|
||||
pad:update_balance()
|
||||
print(string.format(
|
||||
"Bought #%s. New balance: %.2f €.",
|
||||
id, pad.balance/100))
|
||||
else
|
||||
print("Error while attempting to buy product #"..id..".")
|
||||
end
|
||||
|
||||
elseif tonumber(arg[1]) ~= nil then
|
||||
-- Add to (or subtract from) balance.
|
||||
pad:init()
|
||||
if pad:update_balance() then
|
||||
new = math.tointeger(pad.balance + tonumber(arg[1])*100)
|
||||
if pad:set_balance(new) == "OK" then
|
||||
pad:update_balance()
|
||||
print(string.format(
|
||||
"New balance of user %s: %.2f €", pad.USER, pad.balance/100))
|
||||
else
|
||||
print("Sorry, something went wrong. (Unexpected reply from API.)")
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
-- Find and buy item.
|
||||
local roster, query, input, ok
|
||||
roster = pad:get_roster()
|
||||
query = string.lower(table.concat(arg, " "))
|
||||
local menu = {}
|
||||
|
||||
for i = 1, #roster do
|
||||
local item = roster[i]
|
||||
if string.match(string.lower(item.name), query) then
|
||||
table.insert(menu, item)
|
||||
end
|
||||
end
|
||||
if #menu == 0 then
|
||||
print("Sorry, no matching item found.")
|
||||
else
|
||||
print("Press [y] to accept, [n] for next or anything else to cancel:")
|
||||
end
|
||||
|
||||
-- Sort menu by id.
|
||||
table.sort(menu, function (e1, e2) return e1.id < e2.id end)
|
||||
|
||||
-- Check if we can invoke stty.
|
||||
is_tty = os.execute("tty -s") == true
|
||||
|
||||
for i, item in ipairs(menu) do
|
||||
io.stdout:write(string.format("[%d/%d] Buy %s (#%d) for %.2f €? ",
|
||||
i, #menu, item.name, item.id, item.price/100))
|
||||
|
||||
-- Turn off canonical mode, so we can read a char without the user
|
||||
-- having to press enter.
|
||||
if is_tty then os.execute("stty -icanon") end
|
||||
ok, input = pcall(io.read, 1)
|
||||
if is_tty then os.execute("stty icanon") end
|
||||
|
||||
if not ok then break end -- ^C
|
||||
if input == "y" then
|
||||
pad:init()
|
||||
if pad:buy(item.id) == "OK" then
|
||||
pad:update_balance()
|
||||
print(string.format(
|
||||
"\nBought %s. New balance: %.2f €.",
|
||||
item.name, pad.balance/100))
|
||||
end
|
||||
break
|
||||
elseif input == "n" then print() -- force newline
|
||||
elseif input ~= "\n" then
|
||||
print("\nCancelled.")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in a new issue