absurdor (8113B)
1 #!/usr/bin/env lua5.4 2 3 local argparse = require "argparse" 4 local json = require "json" 5 local path = require "path" 6 local fs = require "path.fs" 7 local record = require "lib.record" 8 local pprint = require("pprint").pprint 9 local date = require "date" 10 11 require "lib.util" 12 require "lib.log" 13 14 local parser = argparse("absurdor", "Manage Absurdor duties") 15 :command_target("command") 16 local commands = {} 17 18 commands.report = parser:command("report", "Generate and send absurdor report") 19 commands.report:flag("-p", "Print report, but do not send") 20 commands.report:flag("-d", "Plot the difference, not the height") 21 commands.report:option("-t", "Which template to use") 22 :default("templates/banner.m4") 23 :argname("template") 24 :target("template") 25 commands.record = parser:command("record", "Record events") 26 :command_target("what") 27 commands.push = commands.record:command("push", "A push of The Boulder") 28 commands.push:argument("who", "Name of the player") 29 commands.push:argument("when", "Timestamp of boulder push") 30 :convert(maildate) 31 commands.push:option("-m", "Message ID where push happened"):target("where") 32 commands.log = parser:command("log", "Display log") 33 34 commands.transfer = commands.record:command("transfer", "Transfer the Veblen") 35 commands.transfer:argument("who", "Name of the player") 36 commands.transfer:argument("when", "When the transfer took place") 37 :convert(maildate) 38 commands.transfer:argument("payed", "Amount spent on the transfer") 39 :convert(tonumber) 40 commands.transfer:option("-m", "Message ID where transfer happened"):target("where") 41 42 commands.devalue = commands.record:command("devalue", "Devalue the Veblen") 43 commands.devalue:argument("who", "Name of the player") 44 commands.devalue:argument("when", "When the devalueing took place") 45 :convert(maildate) 46 commands.devalue:option("-m", "Message ID where devalue happened"):target("where") 47 48 local args = parser:parse() 49 50 fn = "log.json" 51 log = decodewith(json.decode, fn) 52 53 if args.command == "report" then 54 55 height = 0 56 local plot = {} 57 local pushed = {} 58 local max = 1 59 local failed = false 60 local who = {} 61 local players = {} 62 local veblen = { 63 cost = 1, 64 history = {}, 65 namewidth = 0 66 } 67 local last = 0 68 local slope 69 local maxslope = 1 70 local maxheight = 0 71 72 if #log > 0 then 73 start = unix2week(log[1].when) 74 max = start 75 end 76 77 for i,e in ipairs(log) do 78 if e.what == "push" then 79 local w = unix2week(e.when) 80 if (unix2week(1739754105) <= w and plot[w] == 0 and plot[unix2week(e.when - 7*24*60*60)] < slope) or 81 (unix2week(1693159683) <= w and w < unix2week(1739754105) and not (pushed[unix2week(e.when - 7*24*60*60)] or pushed[w])) or 82 (w < unix2week(1693159683) and height == 100) 83 -- At 1739754105 seconds from Unix epoch, the governing rule was changed 84 -- so that the Boulder falls to zero if it was not pushed as much as its 85 -- slope the previous week. However, the change is not retroactive; hence 86 -- the magic number. 87 88 -- At 1693159683 seconds from Unix epoch, the governing 89 -- rule was changed so that the Boulder falls to zero 90 -- if it was not pushed the previous week. However, the 91 -- change is not retroactive; hence the magic number. 92 then 93 height = 1 94 slope = 1 95 else 96 height = (height + 1) 97 maxheight = math.max(height, maxheight) 98 end 99 plot[w] = (plot[w] or 0) + 1 100 if slope and plot[w] > slope then 101 slope = plot[w] 102 if e.when > 1739754105 then 103 maxslope = math.max(slope, maxslope) 104 end 105 end 106 who[w] = who[w] or {} 107 table.insert(who[w], e.who) 108 pushed[w] = true 109 if height >= plot[max] then 110 max = w 111 end 112 if players[e.who] then 113 players[e.who] = players[e.who] + 1 114 else 115 players[e.who] = 1 116 end 117 elseif e.what == "transfer" then 118 die(e.payed < veblen.cost, string.format("Recorded transfer by %s used less spendies (%d) than the current Veblen cost (%s)", e.who, e.payed, veblen.cost)) 119 if veblen.current then table.insert(veblen.history, veblen.current) end 120 veblen.current = {who = e.who, payed = e.payed, cost = veblen.cost, when = e.when, what = "transfer"} 121 veblen.cost = e.payed + 1 122 veblen.namewidth = math.max(veblen.namewidth, string.len(e.who)) 123 elseif e.what == "devalue" then 124 local val = math.ceil(veblen.cost/2) 125 table.insert(veblen.history, {when = e.when, what = "devalue", value = e.value}) 126 veblen.cost = e.value 127 elseif e.what == "report" then 128 veblen.cost = e.cost or veblen.cost 129 height = e.height or e.height 130 last = e.when 131 slope = e.slope or 1 132 end 133 end 134 135 local news = {} 136 for i,e in ipairs(log) do 137 if e.when > last then 138 table.insert(news, e) 139 end 140 end 141 142 table.insert(veblen.history, veblen.current) 143 144 local t = os.time() 145 local w = unix2week(t) 146 if (not (w == 0)) and (not pushed[unix2week(t - 7*24*60*60)]) then 147 failed = true 148 if not pushed[w] then 149 height = 0 150 plot[w] = 0 151 end 152 end 153 154 vars = { 155 YYYY = os.date("!%Y"), 156 MM = os.date("!%m"), 157 DD = os.date("!%d"), 158 N = height, 159 K = slope, 160 MN = maxheight, 161 MK = maxslope 162 } 163 164 defs = "" 165 166 for k, v in pairs(vars) do 167 defs = defs .. string.format(" --define=%s=%s", k, v) 168 end 169 170 tmpname = ".tmp" 171 -- Height banner 172 os.execute(string.format("m4 %s %s >> %s", defs, args.template, tmpname)) 173 174 f = io.open(tmpname, "a") 175 176 f:write("EVENTS SINCE LAST REPORT\n") 177 table.sort(news, function(e0, e1) return e0.when > e1.when end) 178 for i,e in ipairs(news) do 179 f:write(fmt_event(e) .. "\n") 180 end 181 f:write("\n") 182 183 -- Top pushers 184 f:write("TOP PUSHERS\n") 185 local scores = {} 186 for p,v in pairs(players) do 187 table.insert(scores, {player = p, pushes = v}) 188 end 189 table.sort(scores, function (s0, s1) return s0.pushes > s1.pushes end) 190 for i,s in ipairs(scores) do 191 f:write(string.format("#%02d %2d %s %s\n", i, s.pushes, string.rep("=", s.pushes), s.player)) 192 end 193 f:write("\n") 194 195 f:write("\n----------------------------------------------------------------------\nTHE VEBLEN\n\n") 196 f:write(" The Veblen\n") 197 f:write(string.format(" is owned by %s\n", veblen.current.who)) 198 f:write(string.format(" and costs %d spendies\n\n", veblen.cost)) 199 200 f:write(" It is shiny.\n") 201 f:write(" It is round (and thus pointless).\n") 202 f:write(" It is admired.\n") 203 f:write(" It has infinite points and is thus no longer pointless.\n") 204 f:write(" (N.B. do note that that's a circle)\n") 205 f:write("\n") 206 207 f:write("HISTORY\n") 208 table.sort(veblen.history, function(x,y) return x.when > y.when end) 209 for _,e in ipairs(veblen.history) do 210 if e.what == "transfer" then 211 f:write(string.format("[%s] %s %s%s\n", os.date("!%Y-%m-%d %H:%M %z", e.when), string.rep(" ", veblen.namewidth - string.len(e.who)) .. e.who, string.rep("$", e.cost), string.rep("+", e.payed - e.cost))) 212 elseif e.what == "devalue" then 213 f:write(string.format("[%s] %s %s\n", os.date("!%Y-%m-%d %H:%M %z", e.when), string.rep(" ", veblen.namewidth), string.rep("$", e.value))) 214 end 215 end 216 f:write('[2024-07-18 02:59 +0000] The Veblen is created') 217 f:write("\n\n") 218 219 f:write("----------------------------------------------------------------------\n") 220 f:write("Do you have any suggestions on what I should put on the report?\n") 221 f:write("Send them to me!\n") 222 f:write("======================================================================\n") 223 f:close() 224 225 if args.p then 226 os.execute(string.format("cat %s", tmpname)) 227 os.remove(tmpname) 228 else 229 os.execute(string.format("neomutt -H %s -E", tmpname)) 230 231 if yn("Archive report? [Yn] ") then 232 table.insert(log, { 233 when = os.time(), 234 what = "report", 235 height = height, 236 cost = veblen.cost, 237 owner = veblen.current.who 238 }) 239 os.rename(tmpname, string.format("archive/%s", os.date("!%F"))) 240 else 241 os.remove(tmpname) 242 end 243 end 244 elseif args.command == "record" then 245 record[args.what](io.stdout, args, log) 246 elseif args.command == "log" then 247 for _,e in ipairs(log) do 248 io.write(fmt_event(e).."\n") 249 end 250 end 251 252 encodewith(json.encode, fn, log) 253 os.execute(string.format('jq --sort-keys . %s > .tmp', fn)) 254 os.execute(string.format('mv .tmp %s', fn))