initialize
This commit is contained in:
commit
5b23ea99fd
43 changed files with 2336 additions and 0 deletions
128
RSS/feeds.lua
Normal file
128
RSS/feeds.lua
Normal file
|
@ -0,0 +1,128 @@
|
|||
local FEEDS = {
|
||||
feeds = {}
|
||||
}
|
||||
|
||||
local FEEDS_PATH = "./feeds"
|
||||
local SLAXML = require('slaxml')
|
||||
local LOG = require('log')
|
||||
|
||||
-- Retrieve feeds from config files and prepare them for use
|
||||
function FEEDS:Load()
|
||||
self.feeds = {}
|
||||
|
||||
local files = fs.find(FEEDS_PATH .. "/*.json")
|
||||
for _, v in ipairs(files) do
|
||||
local file = fs.open(v, "r")
|
||||
local contents = file.readAll()
|
||||
local feed = textutils.unserialiseJSON(contents)
|
||||
|
||||
local response, err, _ = http.get(feed.link)
|
||||
if response == nil then
|
||||
LOG:Error("HTTP Error loading feed (" .. v .. "): " .. err)
|
||||
else
|
||||
local xml = response.readAll()
|
||||
response.close()
|
||||
local element
|
||||
local in_item = false
|
||||
local function startElement(name, nsURI, nsPrefix)
|
||||
-- Track the current element name
|
||||
element = name
|
||||
if element == "item" then
|
||||
in_item = true
|
||||
end
|
||||
end
|
||||
local function text(text, cdata)
|
||||
-- Store the text content based on the current element name
|
||||
if element == 'title' and not feed.title then
|
||||
feed.title = text
|
||||
elseif in_item and not feed.last and element == 'guid' then
|
||||
feed.last = text
|
||||
end
|
||||
end
|
||||
local parser = SLAXML:parser{
|
||||
startElement = startElement,
|
||||
text = text
|
||||
}
|
||||
parser:parse(xml)
|
||||
|
||||
if feed.title_override then
|
||||
feed.title = feed.title_override
|
||||
end
|
||||
|
||||
table.insert(self.feeds, feed)
|
||||
LOG:Info("Loaded feed " .. feed.title)
|
||||
end
|
||||
end
|
||||
if #self.feeds == 0 then
|
||||
LOG:Info("No feeds loaded, nothing to do")
|
||||
return true
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function FEEDS:ParseLatest()
|
||||
local output = {}
|
||||
for _, feed in ipairs(self.feeds) do
|
||||
local response, err, _ = http.get(feed.link)
|
||||
if response == nil then
|
||||
LOG:Error("HTTP Error fetching feed: " .. err)
|
||||
else
|
||||
local xml = response.readAll()
|
||||
response.close()
|
||||
|
||||
local element
|
||||
local new_last
|
||||
local done = false
|
||||
local current_item = {
|
||||
colour = feed.colour,
|
||||
source = feed.title
|
||||
}
|
||||
local in_item = false
|
||||
local function startElement(name, nsURI, nsPrefix)
|
||||
if done then return end
|
||||
|
||||
-- Track the current element name
|
||||
element = name
|
||||
if element == "item" then
|
||||
in_item = true
|
||||
current_item = {
|
||||
colour = feed.colour,
|
||||
source = feed.title
|
||||
}
|
||||
end
|
||||
end
|
||||
local function closeElement(name, nsURI, nsPrefix)
|
||||
if done then return end
|
||||
|
||||
if name == 'item' and not done then
|
||||
table.insert(output, current_item)
|
||||
end
|
||||
end
|
||||
local function text(text, cdata)
|
||||
if done or not in_item then return end
|
||||
|
||||
-- Store the text content based on the current element name
|
||||
if element == 'title' then
|
||||
current_item.title = text
|
||||
elseif in_item and element == 'guid' then
|
||||
if not new_last then
|
||||
new_last = text
|
||||
end
|
||||
if feed.last == text then
|
||||
done = true
|
||||
end
|
||||
end
|
||||
end
|
||||
local parser = SLAXML:parser{
|
||||
startElement = startElement,
|
||||
closeElement = closeElement,
|
||||
text = text
|
||||
}
|
||||
parser:parse(xml)
|
||||
feed.last = new_last
|
||||
end
|
||||
end
|
||||
return output
|
||||
end
|
||||
|
||||
return FEEDS
|
5
RSS/feeds/nytimes.json
Normal file
5
RSS/feeds/nytimes.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"link": "https://rss.nytimes.com/services/xml/rss/nyt/Technology.xml",
|
||||
"title_override": "NYT",
|
||||
"colour": "9"
|
||||
}
|
5
RSS/feeds/tass.json
Normal file
5
RSS/feeds/tass.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"link": "https://tass.ru/rss/v2.xml",
|
||||
"title_override": "TACC",
|
||||
"colour": "b"
|
||||
}
|
5
RSS/feeds/youtube_generic.json
Normal file
5
RSS/feeds/youtube_generic.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"link": "https://www.youtube.com/@tag",
|
||||
"title_override": "Youtuber name",
|
||||
"colour": "e"
|
||||
}
|
71
RSS/graphics.lua
Normal file
71
RSS/graphics.lua
Normal file
|
@ -0,0 +1,71 @@
|
|||
local GRAPHICS = {
|
||||
target = nil,
|
||||
contents = {}
|
||||
}
|
||||
|
||||
local LOG = require('log')
|
||||
|
||||
-- Set target's colorscheme to gruvbox for better visuals
|
||||
function GRAPHICS:SetColours()
|
||||
if self.target == nil then
|
||||
return
|
||||
end
|
||||
self.target.setPaletteColour(colours.red, 0xfb4934)
|
||||
self.target.setPaletteColour(colours.blue, 0x83a598)
|
||||
self.target.setPaletteColour(colours.green, 0xb8bb26)
|
||||
self.target.setPaletteColour(colours.purple, 0xd3869b)
|
||||
self.target.setPaletteColour(colours.cyan, 0x8ec07c)
|
||||
self.target.setPaletteColour(colours.white, 0xf9f5d7)
|
||||
self.target.setPaletteColour(colours.black, 0x1d2021)
|
||||
end
|
||||
|
||||
-- Set target output for our graphics
|
||||
function GRAPHICS:SetTarget(target)
|
||||
self.target = target
|
||||
if self.target == nil then
|
||||
LOG:Error("Nil display target, items will not display")
|
||||
end
|
||||
target.clear()
|
||||
target.setTextScale(0.5)
|
||||
self:Refresh(true)
|
||||
end
|
||||
|
||||
function GRAPHICS:Refresh(full)
|
||||
if self.target == nil then
|
||||
return
|
||||
end
|
||||
local _, y = self.target.getSize()
|
||||
while #self.contents > y do
|
||||
table.remove(self.contents, 1)
|
||||
end
|
||||
|
||||
if full then
|
||||
self.target.scroll(#self.contents)
|
||||
for k, entry in ipairs(self.contents) do
|
||||
self.target.setCursorPos(1, y - k + 1)
|
||||
local output = "[" .. entry.source .. "] " .. entry.title
|
||||
local fg_blit = "0" .. (entry.colour):rep(#entry.source) .. ("0"):rep(#entry.title + 2)
|
||||
self.target.blit(output, fg_blit, ("f"):rep(#output))
|
||||
end
|
||||
else
|
||||
self.target.scroll(1)
|
||||
self.target.setCursorPos(1, y)
|
||||
local entry = self.contents[#self.contents]
|
||||
local output = "[" .. entry.source .. "] " .. entry.title
|
||||
local fg_blit = "0" .. (entry.colour):rep(#entry.source) .. ("0"):rep(#entry.title + 2)
|
||||
self.target.blit(output, fg_blit, ("f"):rep(#output))
|
||||
end
|
||||
end
|
||||
|
||||
function GRAPHICS:AddEntry(entry)
|
||||
table.insert(self.contents, entry)
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
function GRAPHICS:Add(entries)
|
||||
for _, entry in ipairs(entries) do
|
||||
self:AddEntry(entry)
|
||||
end
|
||||
end
|
||||
|
||||
return GRAPHICS
|
98
RSS/log.lua
Normal file
98
RSS/log.lua
Normal file
|
@ -0,0 +1,98 @@
|
|||
local LOG = {
|
||||
target = term,
|
||||
history = {},
|
||||
history_cutoff = 50,
|
||||
log_path = "./logs/",
|
||||
debug = false
|
||||
}
|
||||
|
||||
-- Set target's colorscheme to gruvbox for better visuals
|
||||
function LOG:SetColours()
|
||||
self.target.setPaletteColour(colours.red, 0xfb4934)
|
||||
self.target.setPaletteColour(colours.blue, 0x83a598)
|
||||
self.target.setPaletteColour(colours.green, 0xb8bb26)
|
||||
self.target.setPaletteColour(colours.purple, 0xd3869b)
|
||||
self.target.setPaletteColour(colours.cyan, 0x8ec07c)
|
||||
self.target.setPaletteColour(colours.white, 0xf9f5d7)
|
||||
self.target.setPaletteColour(colours.black, 0x1d2021)
|
||||
end
|
||||
|
||||
-- Set target output for our log
|
||||
function LOG:SetTarget(target)
|
||||
self.target = target
|
||||
target.clear()
|
||||
target.setTextScale(0.5)
|
||||
end
|
||||
|
||||
function LOG:SetDebug(value)
|
||||
self.debug = value
|
||||
end
|
||||
|
||||
-- Utility function to output some kind of log message
|
||||
local function genericLog(log, level, message, col)
|
||||
if #level ~= 4 or #col ~= 1 then
|
||||
LOG:Error("Incorrect generic log format")
|
||||
return
|
||||
end
|
||||
|
||||
local target = log.target
|
||||
target.scroll(1)
|
||||
local _, y = target.getSize()
|
||||
target.setCursorPos(1, y)
|
||||
|
||||
local time = os.date("%T", os.epoch("local") / 1000)
|
||||
local output = "[" .. time .. "|" .. level .. "] " .. message
|
||||
local fg_blit = ("0"):rep(#time + 2) .. (col):rep(4) .. ("0"):rep(#message + 2)
|
||||
target.blit(output, fg_blit, ("f"):rep(#output))
|
||||
|
||||
table.insert(log.history, output)
|
||||
if #log.history > log.history_cutoff then
|
||||
table.remove(log.history, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function LOG:Info(message)
|
||||
genericLog(self, "INFO", message, "b")
|
||||
end
|
||||
|
||||
function LOG:Success(message)
|
||||
genericLog(self, "SCCS", message, "d")
|
||||
end
|
||||
|
||||
function LOG:Debug(message)
|
||||
if not self.debug then return end
|
||||
genericLog(self, "DBUG", message, "8")
|
||||
end
|
||||
|
||||
function LOG:Warning(message)
|
||||
genericLog(self, "WARN", message, "4")
|
||||
end
|
||||
|
||||
function LOG:Error(message)
|
||||
genericLog(self, "ERRO", message, "e")
|
||||
end
|
||||
|
||||
function LOG:Critical(message)
|
||||
genericLog(self, "CRIT", message, "e")
|
||||
self:Dump()
|
||||
error("critical exception occured, read above")
|
||||
end
|
||||
|
||||
function LOG:Clear()
|
||||
self.history = {}
|
||||
end
|
||||
|
||||
-- Put all recent log messages into a file for later study
|
||||
function LOG:Dump()
|
||||
local out = ""
|
||||
for _, v in ipairs(self.history) do
|
||||
out = out + v .. "\n"
|
||||
end
|
||||
local time = os.date("%F-%T", os.epoch("local") / 1000)
|
||||
local file = fs.open(string.format("%s/dump_%s.log", self.log_path, time), "w")
|
||||
file.write(out)
|
||||
file.close()
|
||||
return out
|
||||
end
|
||||
|
||||
return LOG
|
28
RSS/main.lua
Normal file
28
RSS/main.lua
Normal file
|
@ -0,0 +1,28 @@
|
|||
local LOG = require('log')
|
||||
local GRAPHICS = require('graphics')
|
||||
local FEEDS = require('feeds')
|
||||
|
||||
local REFRESH_INTERVAL = 8
|
||||
local DEBUG = true
|
||||
local MONITOR = assert(peripheral.find('monitor'), "No monitor detected")
|
||||
|
||||
local function main()
|
||||
LOG:Info("RSS System init")
|
||||
LOG:SetDebug(DEBUG)
|
||||
LOG:SetTarget(term)
|
||||
LOG:SetColours()
|
||||
GRAPHICS:SetTarget(MONITOR)
|
||||
GRAPHICS:SetColours()
|
||||
local err = FEEDS:Load()
|
||||
if err ~= nil then
|
||||
return
|
||||
end
|
||||
LOG:Success("System fully loaded, beginning RSS processing")
|
||||
|
||||
while true do
|
||||
GRAPHICS:Add(FEEDS:ParseLatest())
|
||||
sleep(REFRESH_INTERVAL)
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
259
RSS/slaxml.lua
Normal file
259
RSS/slaxml.lua
Normal file
|
@ -0,0 +1,259 @@
|
|||
--[=====================================================================[
|
||||
v0.8 Copyright © 2013-2018 Gavin Kistner <!@phrogz.net>; MIT Licensed
|
||||
See http://github.com/Phrogz/SLAXML for details.
|
||||
--]=====================================================================]
|
||||
local SLAXML = {
|
||||
VERSION = "0.8",
|
||||
_call = {
|
||||
pi = function(target,content)
|
||||
print(string.format("<?%s %s?>",target,content))
|
||||
end,
|
||||
comment = function(content)
|
||||
print(string.format("<!-- %s -->",content))
|
||||
end,
|
||||
startElement = function(name,nsURI,nsPrefix)
|
||||
io.write("<")
|
||||
if nsPrefix then io.write(nsPrefix,":") end
|
||||
io.write(name)
|
||||
if nsURI then io.write(" (ns='",nsURI,"')") end
|
||||
print(">")
|
||||
end,
|
||||
attribute = function(name,value,nsURI,nsPrefix)
|
||||
io.write(' ')
|
||||
if nsPrefix then io.write(nsPrefix,":") end
|
||||
io.write(name,'=',string.format('%q',value))
|
||||
if nsURI then io.write(" (ns='",nsURI,"')") end
|
||||
io.write("\n")
|
||||
end,
|
||||
text = function(text,cdata)
|
||||
print(string.format(" %s: %q",cdata and 'cdata' or 'text',text))
|
||||
end,
|
||||
closeElement = function(name,nsURI,nsPrefix)
|
||||
io.write("</")
|
||||
if nsPrefix then io.write(nsPrefix,":") end
|
||||
print(name..">")
|
||||
end,
|
||||
}
|
||||
}
|
||||
|
||||
function SLAXML:parser(callbacks)
|
||||
return { _call=callbacks or self._call, parse=SLAXML.parse }
|
||||
end
|
||||
|
||||
function SLAXML:parse(xml,options)
|
||||
if not options then options = { stripWhitespace=false } end
|
||||
|
||||
-- Cache references for maximum speed
|
||||
local find, sub, gsub, char, push, pop, concat = string.find, string.sub, string.gsub, string.char, table.insert, table.remove, table.concat
|
||||
local first, last, match1, match2, match3, pos2, nsURI
|
||||
local unpack = unpack or table.unpack
|
||||
local pos = 1
|
||||
local state = "text"
|
||||
local textStart = 1
|
||||
local currentElement={}
|
||||
local currentAttributes={}
|
||||
local currentAttributeCt -- manually track length since the table is re-used
|
||||
local nsStack = {}
|
||||
local anyElement = false
|
||||
|
||||
local utf8markers = { {0x7FF,192}, {0xFFFF,224}, {0x1FFFFF,240} }
|
||||
local function utf8(decimal) -- convert unicode code point to utf-8 encoded character string
|
||||
if decimal<128 then return char(decimal) end
|
||||
local charbytes = {}
|
||||
for bytes,vals in ipairs(utf8markers) do
|
||||
if decimal<=vals[1] then
|
||||
for b=bytes+1,2,-1 do
|
||||
local mod = decimal%64
|
||||
decimal = (decimal-mod)/64
|
||||
charbytes[b] = char(128+mod)
|
||||
end
|
||||
charbytes[1] = char(vals[2]+decimal)
|
||||
return concat(charbytes)
|
||||
end
|
||||
end
|
||||
end
|
||||
local entityMap = { ["lt"]="<", ["gt"]=">", ["amp"]="&", ["quot"]='"', ["apos"]="'" }
|
||||
local entitySwap = function(orig,n,s) return entityMap[s] or n=="#" and utf8(tonumber('0'..s)) or orig end
|
||||
local function unescape(str) return gsub( str, '(&(#?)([%d%a]+);)', entitySwap ) end
|
||||
|
||||
local function finishText()
|
||||
if first>textStart and self._call.text then
|
||||
local text = sub(xml,textStart,first-1)
|
||||
if options.stripWhitespace then
|
||||
text = gsub(text,'^%s+','')
|
||||
text = gsub(text,'%s+$','')
|
||||
if #text==0 then text=nil end
|
||||
end
|
||||
if text then self._call.text(unescape(text),false) end
|
||||
end
|
||||
end
|
||||
|
||||
local function findPI()
|
||||
first, last, match1, match2 = find( xml, '^<%?([:%a_][:%w_.-]*) ?(.-)%?>', pos )
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.pi then self._call.pi(match1,match2) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findComment()
|
||||
first, last, match1 = find( xml, '^<!%-%-(.-)%-%->', pos )
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.comment then self._call.comment(match1) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function nsForPrefix(prefix)
|
||||
if prefix=='xml' then return 'http://www.w3.org/XML/1998/namespace' end -- http://www.w3.org/TR/xml-names/#ns-decl
|
||||
for i=#nsStack,1,-1 do if nsStack[i][prefix] then return nsStack[i][prefix] end end
|
||||
error(("Cannot find namespace for prefix %s"):format(prefix))
|
||||
end
|
||||
|
||||
local function startElement()
|
||||
anyElement = true
|
||||
first, last, match1 = find( xml, '^<([%a_][%w_.-]*)', pos )
|
||||
if first then
|
||||
currentElement[2] = nil -- reset the nsURI, since this table is re-used
|
||||
currentElement[3] = nil -- reset the nsPrefix, since this table is re-used
|
||||
finishText()
|
||||
pos = last+1
|
||||
first,last,match2 = find(xml, '^:([%a_][%w_.-]*)', pos )
|
||||
if first then
|
||||
currentElement[1] = match2
|
||||
currentElement[3] = match1 -- Save the prefix for later resolution
|
||||
match1 = match2
|
||||
pos = last+1
|
||||
else
|
||||
currentElement[1] = match1
|
||||
for i=#nsStack,1,-1 do if nsStack[i]['!'] then currentElement[2] = nsStack[i]['!']; break end end
|
||||
end
|
||||
currentAttributeCt = 0
|
||||
push(nsStack,{})
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findAttribute()
|
||||
first, last, match1 = find( xml, '^%s+([:%a_][:%w_.-]*)%s*=%s*', pos )
|
||||
if first then
|
||||
pos2 = last+1
|
||||
first, last, match2 = find( xml, '^"([^<"]*)"', pos2 ) -- FIXME: disallow non-entity ampersands
|
||||
if first then
|
||||
pos = last+1
|
||||
match2 = unescape(match2)
|
||||
else
|
||||
first, last, match2 = find( xml, "^'([^<']*)'", pos2 ) -- FIXME: disallow non-entity ampersands
|
||||
if first then
|
||||
pos = last+1
|
||||
match2 = unescape(match2)
|
||||
end
|
||||
end
|
||||
end
|
||||
if match1 and match2 then
|
||||
local currentAttribute = {match1,match2}
|
||||
local prefix,name = string.match(match1,'^([^:]+):([^:]+)$')
|
||||
if prefix then
|
||||
if prefix=='xmlns' then
|
||||
nsStack[#nsStack][name] = match2
|
||||
else
|
||||
currentAttribute[1] = name
|
||||
currentAttribute[4] = prefix
|
||||
end
|
||||
else
|
||||
if match1=='xmlns' then
|
||||
nsStack[#nsStack]['!'] = match2
|
||||
currentElement[2] = match2
|
||||
end
|
||||
end
|
||||
currentAttributeCt = currentAttributeCt + 1
|
||||
currentAttributes[currentAttributeCt] = currentAttribute
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findCDATA()
|
||||
first, last, match1 = find( xml, '^<!%[CDATA%[(.-)%]%]>', pos )
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.text then self._call.text(match1,true) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function closeElement()
|
||||
first, last, match1 = find( xml, '^%s*(/?)>', pos )
|
||||
if first then
|
||||
state = "text"
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
|
||||
-- Resolve namespace prefixes AFTER all new/redefined prefixes have been parsed
|
||||
if currentElement[3] then currentElement[2] = nsForPrefix(currentElement[3]) end
|
||||
if self._call.startElement then self._call.startElement(unpack(currentElement)) end
|
||||
if self._call.attribute then
|
||||
for i=1,currentAttributeCt do
|
||||
if currentAttributes[i][4] then currentAttributes[i][3] = nsForPrefix(currentAttributes[i][4]) end
|
||||
self._call.attribute(unpack(currentAttributes[i]))
|
||||
end
|
||||
end
|
||||
|
||||
if match1=="/" then
|
||||
pop(nsStack)
|
||||
if self._call.closeElement then self._call.closeElement(unpack(currentElement)) end
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findElementClose()
|
||||
first, last, match1, match2 = find( xml, '^</([%a_][%w_.-]*)%s*>', pos )
|
||||
if first then
|
||||
nsURI = nil
|
||||
for i=#nsStack,1,-1 do if nsStack[i]['!'] then nsURI = nsStack[i]['!']; break end end
|
||||
else
|
||||
first, last, match2, match1 = find( xml, '^</([%a_][%w_.-]*):([%a_][%w_.-]*)%s*>', pos )
|
||||
if first then nsURI = nsForPrefix(match2) end
|
||||
end
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.closeElement then self._call.closeElement(match1,nsURI) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
pop(nsStack)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
while pos<#xml do
|
||||
if state=="text" then
|
||||
if not (findPI() or findComment() or findCDATA() or findElementClose()) then
|
||||
if startElement() then
|
||||
state = "attributes"
|
||||
else
|
||||
first, last = find( xml, '^[^<]+', pos )
|
||||
pos = (first and last or pos) + 1
|
||||
end
|
||||
end
|
||||
elseif state=="attributes" then
|
||||
if not findAttribute() then
|
||||
if not closeElement() then
|
||||
error("Was in an element and couldn't find attributes or the close.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not anyElement then error("Parsing did not discover any elements") end
|
||||
if #nsStack > 0 then error("Parsing ended with unclosed elements") end
|
||||
end
|
||||
|
||||
return SLAXML
|
3
RSS/todo.txt
Normal file
3
RSS/todo.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
* Add test file that checks entire system/functions before startup? or at least improve error checking
|
||||
* Enhance customisation per-feed e.g. define 14 colours and set which color each char in feed title is
|
||||
* Pre-Processing for russian language
|
Loading…
Add table
Add a link
Reference in a new issue