-- *********************************************************************** -- -- Copyright 2020 by Sean Conner. -- -- This program is free software: you can redistribute it and/or modify it -- under the terms of the GNU General Public License as published by the -- Free Software Foundation, either version 3 of the License, or (at your -- option) any later version. -- -- This program is distributed in the hope that it will be useful, but -- WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -- Public License for more details. -- -- You should have received a copy of the GNU General Public License along -- with this program. If not, see . -- -- Comments, questions and criticisms can be sent to: sean@conman.org -- -- ======================================================================= -- -- Code to handle blog requests. -- -- *********************************************************************** -- luacheck: globals init handler last_link -- luacheck: ignore 611 local syslog = require "org.conman.syslog" local fsys = require "org.conman.fsys" local magic = require "org.conman.fsys.magic" local date = require "org.conman.date" local wrapt = require "org.conman.string".wrapt local ENTITIES = require "org.conman.const.entity" local url = require "org.conman.parsers.url.data" + require "org.conman.parsers.url.gopher" + require "org.conman.parsers.url" local mimetype = require "org.conman.parsers.mimetype" local gtypes = require "org.conman.const.gopher-types" local lpeg = require "lpeg" local io = require "io" local os = require "os" local table = require "table" local string = require "string" local utf8 = require "utf8" local mklink = require "port70.mklink" local readfile = require "port70.readfile" local html = require "org.conman.app.port70.handlers.blog.html" local uurl = require "org.conman.app.port70.handlers.blog.url-util" local format = require "org.conman.app.port70.handlers.blog.format" local type = type local setfenv = setfenv local require = require local loadfile = loadfile local tonumber = tonumber local tostring = tostring local ipairs = ipairs local _VERSION = _VERSION local blog = { require = setmetatable( {}, { __index = function(s) return s end, __call = function(s) return s end, } ) } if _VERSION == "Lua 5.1" then module("blog") else _ENV = {} end local deent do local char = lpeg.P"&#" * lpeg.C(lpeg.R"09"^1) * lpeg.P";" / utf8.char + lpeg.P"&" * lpeg.C(lpeg.R("az","AZ","09")^1) * lpeg.P";" / ENTITIES + lpeg.S" \t\r\n"^1 / " " + lpeg.P(1) deent = lpeg.Cs(char^0) end -- *********************************************************************** -- usage: date = read_date(fname) -- desc: Read the blog start and end date files. -- input: fname (string) either '.first' or '.last' -- return: date (table) -- * year -- * month -- * day -- *********************************************************************** local number = lpeg.R"09"^1 / tonumber local dateparse = lpeg.Ct( lpeg.Cg(number,"year") * lpeg.P"/" * lpeg.Cg(number,"month") * lpeg.P"/" * lpeg.Cg(number,"day") * lpeg.P"." * lpeg.Cg(number,"part") ) local function read_date(fname) fname = blog.basedir .. "/" .. fname local f = io.open(fname,"r") local d = f:read("*l") f:close() return dateparse:match(d) end -- *********************************************************************** -- usage: titles = get_days_titles(when) -- desc: Retreive the titles of the posts of a given day -- input: when (table) -- * year -- * month -- * day -- * part -- return: titles (string/array) titles for each post -- *********************************************************************** local function get_days_titles(when) local res = {} local fname = string.format("%s/%d/%02d/%02d/titles",blog.basedir,when.year,when.month,when.day) if fsys.access(fname,"r") then for title in io.lines(fname) do table.insert(res,deent:match(title)) end end return res end -- *********************************************************************** -- usage: collect_day(when) -- desc: Create gopher links for a day's entry -- input: when (table) -- * year -- * month -- * day -- *********************************************************************** local function collect_day(when) when.part = 1 local acc = {} local fname = string.format("%s/%d/%02d/%02d/titles",blog.basedir,when.year,when.month,when.day) if fsys.access(fname,"r") then for title in io.lines(fname) do local link = string.format("Phlog:%d/%02d/%02d.%d",when.year,when.month,when.day,when.part) table.insert(acc, mklink { type = 'file', display = deent:match(title), selector = link, }) when.part = when.part + 1 end end return acc end -- *********************************************************************** -- usage: collect_month(when) -- desc: Create gopher links for a month's worth of entries -- input: acc (table) table for accumulating links -- input: when (table) -- * year -- * month -- * day -- *********************************************************************** local function collect_month(when) when.day = 1 local acc = {} local d = os.time(when) table.insert(acc, mklink { type = 'info', display = os.date("%B, %Y",d), }) local maxday = date.daysinmonth(when) for day = 1 , maxday do when.day = day local posts = collect_day(when) if #posts > 0 then local title = string.format("%d/%02d/%02d",when.year,when.month,when.day) local link = string.format("Phlog:%d/%02d/%02d",when.year,when.month,when.day) table.insert(acc, mklink { type = 'dir', display = title, selector = link, }) for _,post in ipairs(posts) do table.insert(acc,post) end end end return acc end -- *********************************************************************** -- LPEG code to parse a request. tumber() will parse the request and return -- a table with the following fields: -- -- * year - year of request -- * month - month of request -- * day - day of request -- * part - part of day -- * file - file reference -- * unit - one of 'none', 'year', 'month' , 'day' , 'part' , 'file' -- indicating how much of a request was made. -- *********************************************************************** local Ct = lpeg.Ct local Cg = lpeg.Cg local Cc = lpeg.Cc local P = lpeg.P local eos = P(-1) local file = P"/" * Cg(P(1)^0,"file") * Cg(Cc('file'), "unit") local part = P"." * Cg(number,"part") * Cg(Cc('part'), "unit") local day = P"/" * Cg(number,"day") * Cg(Cc('day'), "unit") local month = P"/" * Cg(number,"month") * Cg(Cc('month'),"unit") local year = Cg(number,"year") * Cg(Cc('year'), "unit") local tumbler = Ct( year * month * day * file * eos + year * month * day * part * eos + year * month * day * eos + year * month * P"/"^-1 * eos + year * P"/"^-1 * eos + Cg(Cc('none'),"unit") * eos ) * P(-1) -- *********************************************************************** -- usage: links = display(request) -- desc: Return a list of gopher links for a given request -- input: request (string) requested entry/ies -- return: links (array) array of gopher links -- *********************************************************************** local function display_part(what,fpath) local function affiliate(loc) for _,aff in ipairs(blog.affiliate) do if loc.scheme == aff.proto then return string.format(aff.link,loc.path) end end return uurl.toa(loc) end local CONF = require "port70.CONF" local f = io.open(blog.basedir .. fpath,"r") if not f then return false,'not found' end local doc = html(f) f:close() local txt,links = format(doc) local base = url:match(blog.url .. string.format("%04d/%02d/%02d/",what.year,what.month,what.day)) for i,item in ipairs(links) do if item:match "^%d%d%d%d/" then item = "/" .. item end local hugo local location = url:match(item) if location.scheme == 'gopher' then txt = txt .. string.format("[%d] %s\n",i,item) else local abs = uurl.merge(base,location) if abs.host == base.host then local ftype = 'file' local filename = blog.basedir .. abs.path if fsys.access(filename,"r") then local mime = mimetype:match(magic(filename)) if not mime then type = 'binary' else if mime.type:match "^text/html" then ftype = 'html' elseif mime.type:match "^text/" then ftype = 'file' elseif mime.type:match "^image/gif" then ftype = 'gif' elseif mime.type:match "^image/" then ftype = 'image' elseif mime.type:match "^audio/" then ftype = 'sound' else ftype = 'binary' end end end local port do if CONF.network.port ~= 70 then port = ":" .. tostring(CONF.network.port) else port = "" end end hugo = string.format("gopher://%s%s/%sPhlog:%s", CONF.network.host,port, gtypes[ftype], abs.path:sub(2,-1) ) else hugo = affiliate(abs) end txt = txt .. string.format("[%d] %s\n",i,hugo) end end return true,txt end -- *********************************************************************** local function display(request,ext) if not request then return true,{ mklink { type = 'error', display = 'Not found', selector = "" -- XXX hmmm ... not the full selector }} end local what = tumbler:match(request) if not what then return true,{ mklink { type = 'error', display = 'Not found', selector = request }} end local last = read_date(".last") if what.unit == 'none' then local years = {} -- xluacheck: ignore for i = last.year , blog._first.year , -1 do table.insert(years,mklink { type = 'dir', display = tostring(i), selector = "Phlog:" .. i, }) end return true,years elseif what.unit == 'year' then local months = {} local when = { year = 1999 , month = 1 , day = 1 } for i = 1 , 12 do if what.year == blog._first.year and i >= blog._first.month or what.year == last.year and i <= last.month or what.year > blog._first.year and what.year < last.year then when.month = i local d = os.time(when) table.insert(months,mklink { type = 'dir', display = os.date("%B",d), selector = string.format("Phlog:%d/%02d",what.year,i), }) end end return true,months elseif what.unit == 'month' then return true,collect_month(what) elseif what.unit == 'day' then return true,collect_day(what) elseif what.unit == 'part' then local fpath = string.format("/%04d/%02d/%02d/%d",what.year,what.month,what.day,what.part) local titles = get_days_titles(what) if #titles > 0 then local okay,data = display_part(what,fpath) if not okay then return false,data else local t = wrapt(titles[what.part],77) table.insert(t,"") table.insert(t,"* * * * *") table.insert(t,"") -- ------------------ -- Center the title -- ------------------ for i = 1 , #t do t[i] = string.rep(" ",(80 - #t[i]) // 2) .. t[i] end return true, table.concat(t,'\n') --titles[what.part] .. "\r\n" .. data .. "\r\n.\r\n" end else return false,'Not found' end elseif what.unit == 'file' then local fpath = string.format("%s/%04d/%02d/%02d/%s",blog.basedir,what.year,what.month,what.day,what.file) if fpath:match("%.x%-html$") then return display_part(what,fpath) else return readfile(fpath,ext) end else syslog('error',"Um ... what now?") return false,'Not found' end end -- *********************************************************************** -- usage: init() -- desc: Intialize the handler module -- *********************************************************************** function init(conf) local f,err = loadfile(conf.config,"t",blog) if not f then syslog('error',"%s: %s",conf.config,err) return false,err end if _VERSION == "Lua 5.1" then setfenv(f,blog) end f() blog._first = read_date(".first") return true end -- *********************************************************************** function handler(info,request) local okay,data = display(request.match[1],info.extension) if okay then if type(data) == 'table' then return true,table.concat(data) .. ".\r\n" else return true,data end end end -- *********************************************************************** -- usage: link = last_link() -- desc: return a gopher link for the latest blog entry -- return: link (string) gopher link -- *********************************************************************** function last_link() local last = read_date(".last") return string.format( "Phlog:%d/%02d/%02d.%d", last.year, last.month, last.day, last.part ) end -- *********************************************************************** if _VERSION >= "Lua 5.2" then return _ENV end