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