Skip to content
Snippets Groups Projects
Commit 8f335bbb authored by Peter Nerlich's avatar Peter Nerlich
Browse files

first implementation of AK Announcer

parents
No related branches found
No related tags found
No related merge requests found
-- only print messages if players on server
-- print messages privately to player on join
local FETCH_INTERVAL = 4*60 -- 4 minutes
--local FETCH_INTERVAL = 30
local aka = ak_announcer
aka.my_offset = 0
aka.data = {
slots = {},
aks = {},
rooms = {},
categories = {}
}
aka.description = {
slots = {
id = true,
start = true,
duration = true,
fixed = true,
updated = true,
ak = "aks",
room = "rooms",
event = false
},
aks = {
id = true,
name = true,
short_name = true,
description = true,
link = true,
protocol_link = false,
reso = false,
present = false,
notes = false,
interest = true,
interest_counter = true,
category = "categories",
track = false,
event = false,
owners = false,
tags = false,
requirements = false,
conflicts = false,
prerequisites = false
},
rooms = {
id = true,
name = true,
location = true,
capacity = false,
event = false,
properties = false
},
categories = {
id = true,
name = true,
color = false,
description = true,
event = false
}
}
aka.last_announced = {}
local update_job = nil
local http, url
function aka.init(_http, _url)
http = _http
url = _url
--aka.my_offset = os.time() - aka.parseDateTimeToLocal("2020-11-05T09:00Z")
--aka.my_offset = 16243200 -- offset to last kif
minetest.log("info", "[AK-Announcer] init done! starting first update")
aka.update_data()
end
minetest.register_on_joinplayer(function(player, last_login)
if update_job == nil then
aka.update_data()
else
local now = os.time() - aka.my_offset
local tenminsago = now - 60*10
local inonehour = now + 60*60
minetest.log("info", "[AK-Announcer] "..name..": looking for AKs starting between "..aka.pretty_time(tenminsago).." and "..aka.pretty_time(inonehour))
local aks = aka.compile_range(tenminsago, inonehour)
local out = aka.typeset(aks)
if #out > 0 then
minetest.chat_send_player(name, out)
end
end
end)
minetest.register_on_leaveplayer(function(player, timed_out)
if #minetest.get_connected_players() <= 1 then -- leaving player is still counted
if update_job ~= nil then
update_job:cancel()
update_job = nil
end
end
end)
function aka.fetch_data(name, cb)
local api
if name == "slots" then
api = "akslot"
elseif name == "aks" then
api = "ak"
elseif name == "categories" then
api = "akcategory"
elseif name == "rooms" then
api = "room"
end
http.fetch({
url = url .. api .. "/?format=json",
timeout = 5
}, function(res)
local value = minetest.parse_json(res.data)
local new_data = {}
if value ~= nil then
for i,v in ipairs(value) do
new_data[v.id] = v
end
end
minetest.log("info", "[AK-Announcer] fetched "..name.."!")
cb(new_data)
end)
end
function aka.sort_data(name, cb, new_data)
if not new_data then
minetest.log("error", "[AK-Announcer] no new data provided for sorting "..name.."!")
else
local updated = {}
for k,source in pairs(new_data) do
if name == "slots" and source.start == nil then
minetest.log("warning", "start is missing for slot "..source.id)
--minetest.log("warning", aka.pprint(source))
end
if aka.data[name][k] == nil then
aka.data[name][k] = {}
end
local target = aka.data[name][k]
for prop,status in pairs(aka.description[name]) do
if status then
if type(status) == "string" then
if target[prop] ~= aka.data[status][source[prop]] then
target[prop] = aka.data[status][source[prop]]
if updated[k] == nil then
updated[k] = {}
end
updated[k][prop] = target[prop]
end
else
if target[prop] ~= source[prop] then
target[prop] = source[prop]
if prop == "start" or prop == "updated" then
target[prop.."_local"] = aka.parseDateTimeToLocal(target[prop])
end
if updated[k] == nil then
updated[k] = {}
end
updated[k][prop] = target[prop]
end
end
end
end
end
for k,_ in pairs(aka.data[name]) do
if new_data[k] == nil then
aka.data[name][k] = nil
end
end
minetest.log("info", "[AK-Announcer] sorted "..name.."!")
print(aka.pprint(aka.data[name]))
cb(updated)
end
end
function aka.update_data()
local flags = {
fetch = {},
sort = {}
}
local make_callback
make_callback = function(stage, name)
return function(new_data)
flags[stage][name] = new_data
if stage == "fetch" then
-- if no dependencies or all present, run sort stage
local dependencies = {}
for k,v in pairs(aka.description[name]) do
if type(v) == "string" then
table.insert(dependencies, v)
end
end
local breaking = false
if #dependencies > 0 then
for i,v in ipairs(dependencies) do
if not flags[stage][v] then
breaking = true
--minetest.log("info", "[AK-Announcer] not sorting "..name.." yet, still waiting for "..v)
break
end
end
end
if not breaking then
-- next stage for name!
aka.sort_data(name, make_callback("sort", name), flags[stage][name])
end
elseif stage == "sort" then
local found_unsorted = false
-- for all that have us as dependency and are now complete, run sort stage
for other,v in pairs(aka.description) do
if other ~= name then
if flags[stage][other] == nil then
found_unsorted = true
--minetest.log("info", "[AK-Announcer] "..name.." done, checking whether we can trigger "..other)
local dependencies = {}
for _,dep in pairs(aka.description[other]) do
if type(dep) == "string" then
table.insert(dependencies, dep)
end
end
if #dependencies > 0 then
local we_are_in = false
local breaking = false
for i,v in ipairs(dependencies) do
if v == name then
we_are_in = true
elseif not flags[stage][v] then
breaking = true
break
end
end
if not we_are_in then
--minetest.log("info", "[AK-Announcer] "..name.." done, not triggering "..other..", we are not a dependency: "..table.concat(dependencies, ", "))
elseif breaking then
--minetest.log("info", "[AK-Announcer] "..name.." done, not triggering "..other..", still unsorted dependencies in "..table.concat(dependencies, ", "))
elseif flags.fetch[other] == nil then
minetest.log("info", "[AK-Announcer] "..name.." done, not triggering "..other.." which is still fetching")
else
--minetest.log("info", "[AK-Announcer] "..name.." done, triggering sorting of "..other)
-- next stage for other!
aka.sort_data(other, make_callback("sort", other), flags.fetch[other])
end
else
--minetest.log("info", "[AK-Announcer] "..name.." done, "..other.." had no dependencies")
end
else
--minetest.log("info", "[AK-Announcer] "..name.." done, "..other.." already done!")
end
end
end
if not found_unsorted then
-- we done!
--minetest.log("info", "[AK-Announcer] done with everything!")
--minetest.log("info", "[AK-Announcer] "..aka.pprint(aka.data))
aka.announce_upcoming_and_changes(flags[stage])
end
end
end
end
for name,v in pairs(aka.description) do
aka.fetch_data(name, make_callback("fetch", name))
end
--minetest.log("info", "[AK-Announcer] dispatched everything. Will update in "..tostring(FETCH_INTERVAL))
update_job = minetest.after(FETCH_INTERVAL, aka.update_data)
end
function aka.announce_upcoming_and_changes(updated)
local now = os.time() - aka.my_offset
local tenminsago = now - 60*10
local inonehour = now + 60*60
minetest.log("info", "[AK-Announcer] looking for AKs starting between "..aka.pretty_time(tenminsago).." and "..aka.pretty_time(inonehour))
local aks = aka.compile_range(tenminsago, inonehour)
local has_news = false
for k,v in pairs(aks) do
if aka.last_announced[k] == nil then
has_news = true
else
for id,slot in pairs(v) do
if aka.last_announced[k][id] == nil then
has_news = true
break
end
end
end
if has_news then
break
end
end
if has_news then
local out = aka.typeset(aks)
if #out > 0 then
minetest.log("info", "[AK-Announcer] " .. out)
minetest.chat_send_all(out)
end
aka.last_announced = aks
else
minetest.log("info", "[AK-Announcer] no news to send out.")
end
end
function aka.compile_range(starting, ending)
local upcoming = {}
local _keys = {}
for k,slot in pairs(aka.data.slots) do
if slot.start_local == nil then
minetest.log("warning", "[AK-Announcer] ignoring slot, has no start time: "..slot.id) --aka.pprint(slot))
else
if starting <= slot.start_local and slot.start_local <= ending then
--minetest.log("info", "[AK-Announcer] found matching slot at "..slot.start.." : "..aka.pprint(slot))
if upcoming[slot.start_local] == nil then
upcoming[slot.start_local] = {}
end
upcoming[slot.start_local][slot.id] = slot
end
end
end
return upcoming
end
function aka.typeset(aks)
local out = ""
local _keys = {}
for k,v in pairs(aks) do
table.insert(_keys, k)
end
table.sort(_keys)
for _,start_local in ipairs(_keys) do
if #out > 0 then
out = out .. "\n"
end
out = out .. "[ " .. aka.pretty_time(start_local) .. " (" .. aka.pretty_duration(start_local - os.time() + aka.my_offset, true) .. ") ]\n"
--out = out .. "[ " .. aka.pretty_time(start_local) .. " (" .. aka.pretty_duration(start_local - os.time() + aka.my_offset, true) .. " - now: " .. aka.pretty_time(os.time() - aka.my_offset) .. ") ]\n"
for id,slot in pairs(aks[start_local]) do
if not slot.ak then
minetest.log("warning", "[AK-Announcer] AK info not in slot: "..slot.id) --aka.pprint(slot))
else
out = out .. " - " .. slot.ak.short_name .. " »» " .. slot.room.location .. " " .. slot.room.name .. " (" .. slot.ak.category.name .. ")" .. "\n"
end
end
end
return out
end
function aka.pretty_time(ts)
local time = os.date('*t', ts)
return string.format("%02d:%02d Uhr", time.hour, time.min)
end
function aka.pretty_duration(dur, use_in_and_ago)
local tr = {
day = "Tag",
days = "Tage",
hour = "Stunde",
hours = "Stunden",
minute = "Minute",
minutes = "Minuten",
now = "jetzt",
in_start = "in",
in_end = "",
ago_start = "vor",
ago_end = ""
--[[day = "day",
days = "days",
hour = "hour",
hours = "hours",
minute = "minute",
minutes = "minutes",
now = "right now",
in_start = "in",
in_end = "",
ago_start = "",
ago_end = "ago"]]--
}
local s_start = ""
local s_end = ""
if dur < 0 then
dur = -dur
s_end = tr.ago_end
s_start = tr.ago_start
else
s_end = tr.in_end
s_start = tr.in_start
end
local day, hour, min, _
hour, min = math.modf(dur / 60 / 60)
print("modf'ed to hour: "..tostring(hour).." min: "..tostring(min))
if hour >= 24 then
day, hour = math.modf(dur / 24 / 60 / 60)
print("modf'ed to day: "..tostring(day).."hour: "..tostring(hour))
hour = hour * 60
hour = math.floor(hour+.5)
min = 0
else
min = min * 60
if min < 1 then
return tr.now
end
min = math.floor(min+.5)
end
if day and day > 0 then
day = tostring(day) .. " " .. (day == 1 and tr.day or tr.days)
else
day = ""
end
if hour and hour > 0 then
hour = tostring(hour) .. " " .. (hour == 1 and tr.hour or tr.hours)
else
hour = ""
end
if min and min > 0 then
min = tostring(min) .. " " .. (min == 1 and tr.minute or tr.minutes)
else
min = ""
end
if #day > 0 and #hour > 0 then
day = day .. " "
end
if #hour > 0 and #min > 0 then
hour = hour .. " "
end
if #s_start > 0 then
s_start = s_start .. " "
end
if #s_end > 0 then
s_end = " " .. s_end
end
return s_start .. day .. hour .. min .. s_end
end
local aka = ak_announcer
if aka.chat == nil then
aka.chat = {}
end
function aka.chat.upcoming_aks(name)
local player = minetest.get_player_by_name(name)
if player == nil then
return false
end
local now = os.time() - aka.my_offset
local tenminsago = now - 60*10
local inonehour = now + 60*60
minetest.log("info", "[AK-Announcer] "..name..": looking for AKs starting between "..aka.pretty_time(tenminsago).." and "..aka.pretty_time(inonehour))
local aks = aka.compile_range(tenminsago, inonehour)
local out = aka.typeset(aks)
if #out == 0 then
out = "Keine anstehenden AKs. ("..aka.pretty_time(now)..")"
end
minetest.chat_send_player(name, out)
end
function aka.chat.set_offset(name, param)
local parsed = aka.parseDateTimeToLocal(param)
if parsed ~= nil then
aka.my_offset = os.time() - parsed
minetest.chat_send_player(name, "It is now "..aka.pretty_time(os.time() - aka.my_offset))
else
minetest.chat_send_player(name, "Could not parse offset!")
end
end
minetest.register_chatcommand("upcoming_aks", {
description = "Anstehende AKs.",
func = aka.chat.upcoming_aks,
})
--[[minetest.register_chatcommand("set_offset", {
description = "Set offset.",
func = aka.chat.set_offset,
})]]--
init.lua 0 → 100644
ak_announcer = {}
local aka = ak_announcer
local MP = minetest.get_modpath("ak_announcer")
local http = QoS and QoS(minetest.request_http_api(), 2) or minetest.request_http_api()
if http then
--ak_url = "http://ak.kif.rocks/kif485/api/"
ak_url = "http://ak.kif.rocks/kif490/api/"
minetest.log("warning", "[AK-Announcer] starting with endpoint: " .. ak_url)
dofile(MP .. "/util.lua")
dofile(MP .. "/ak_stuff.lua")
dofile(MP .. "/chat_commands.lua")
aka.init(http, ak_url)
else
minetest.log("error", "[AK-Announcer] Using HTTP was denied.")
end
name = ak_announcer
description = Announcing upcoming KIF AKs in chat.
optional_depends = qos
util.lua 0 → 100644
local aka = ak_announcer
-- time parsing stuff from http://lua-users.org/wiki/TimeZone
-- and https://stackoverflow.com/questions/7911322/lua-iso-8601-datetime-parsing-pattern
function aka.parseDateTime(str, timezone)
timezone = aka.get_tzoffset(timezone or aka.get_timezone())
--minetest.log("info", "[AK-Announcer] got timezone of "..aka.pprint(timezone))
local date = aka._parseDate(str)
local time = aka._parseTime(str)
local offset = aka._parseOffset(str)
if date == nil or time == nil or offset == nil then
return nil
end
local tbl = {}
for _,v in ipairs({date, time}) do
for k,w in pairs(v) do
tbl[k] = w
end
end
tbl.hour = tbl.hour + offset.h + timezone.h
tbl.min = tbl.min + offset.m + timezone.m
return os.time(tbl)
end
function aka._parseDate(str)
-- matches YYYYMMDD, YYYY-MM-DD, YYYY-MM or YYYY-DDD (where DDD is the day of the year: 1-365/166); else returns nils
local Y, M, D, DoY
Y, M, D = str:match("^(%d%d%d%d)-?(%d%d)-?(%d%d)")
if Y ~= nil then
return {year = tonumber(Y), month = tonumber(M), day = tonumber(D)}
end
Y, DoY = str:match("^(%d%d%d%d)-(%d%d%d)")
if Y ~= nil then
return {year = tonumber(Y), yday = tonumber(DoY)}
end
Y, M = str:match("^(%d%d%d%d)-(%d%d)")
if Y ~= nil then
return {year = tonumber(Y), month = tonumber(M), day = tonumber(D)}
end
return nil
end
function aka._parseTime(str)
-- matches hh:mm:ss, hh:mm, hh, hhmmss, hhmm, hh:mm,f, hhmm,f, hh:mm.f, or hhmm.f; else returns nils
local h, m, s, f
h, m, s = str:match("T(%d%d):?(%d%d):?(%d%d)")
if h ~= nil then
return {hour = tonumber(h), min = tonumber(m), sec = tonumber(s)}
end
h, m, f = str:match("T(%d%d):?(%d%d)[.,](%d)")
if h ~= nil then
s = tonumber(f) * 60
return {hour = tonumber(h), min = tonumber(m), sec = s}
end
h, m = str:match("T(%d%d):?(%d%d)")
if h ~= nil then
return {hour = tonumber(h), min = tonumber(m), sec = 0}
end
h, f = str:match("T(%d%d)[.,](%d)")
if h ~= nil then
m = tonumber(f) * 60
return {hour = tonumber(h), min = m, sec = 0}
end
h = str:match("T(%d%d)")
if h ~= nil then
return {hour = tonumber(h), min = 0, sec = 0}
end
minetest.log("warning", "[AK-Announcer] failed to parsed time from "..str)
return nil
end
function aka._parseOffset(str)
if str:sub(-1) == "Z" then
return {h = 0, m = 0}
end -- ends with Z, Zulu time
-- matches ±hh:mm, ±hhmm or ±hh; else returns nils
local sign, h, m = str:match("([-+])(%d%d):?(%d?%d?)$")
sign, h, m = sign or "+", h or "00", m or "00"
return {h = tonumber(sign..h), m = tonumber(sign..m)}
end
function aka.get_timezone()
local now = os.time()
return os.difftime(now, os.time(os.date("!*t", now)))
end
function aka.get_tzoffset(timezone)
local h, m = math.modf(timezone / 3600)
return {h = h, m = 60 * m}
end
function aka.parseDateTimeToLocal(str)
return aka.parseDateTime(str, aka.get_timezone())
end
function aka.pprint(value, ind, inline)
local out = ""
ind = ind or 0
local sep = " "
local indent = function(_ind)
_ind = _ind or ind
return sep:rep(ind)
end
if type(value) == "table" then
out = out .. (inline and "" or indent(ind)) .. "{\n"
for k,v in pairs(value) do
out = out .. indent(ind+2) .. "[" .. tostring(k).."] = " .. aka.pprint(v, ind+2, true) .. ",\n"
end
out = out .. indent(ind) .. "}"
elseif type(value) == "string" then
local lb = "\n"
local s = '"' .. value .. '"' .. lb
--local lines = {s:match((s:gsub("[^"..lb.."]*"..lb, "([^"..lb.."]*)"..lb)))}
local lines = {}
for line in s:gmatch("[^"..lb.."]+") do
if #lines == 0 then
table.insert(lines, (inline and "" or indent(ind)) .. line)
else
table.insert(lines, indent(ind) .. line)
end
end
out = out .. table.concat(lines, "\n")
else
out = out .. (inline and "" or indent(ind)) .. tostring(value)
end
return out
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment