-- *********************************************************************** -- -- 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 -- luacheck: ignore 611 631 local syslog = require "org.conman.syslog" local errno = require "org.conman.errno" local nfl = require "org.conman.nfl" local date = require "org.conman.date" local fsys = require "org.conman.fsys" local url = require "org.conman.parsers.url.data" + require "org.conman.parsers.url.gopher" + require "org.conman.parsers.url" local io = require "io" local os = require "os" local table = require "table" local string = require "string" local readfile = require "GLV-1.handlers.file" local html = require "org.conman.app.mod_blog.html" local uurl = require "GLV-1.url-util" -- "org.conman.app.port70.handlers.blog.url-util" local format = require "org.conman.app.GLV-1.handlers.blog.format" local blog = require "org.conman.app.mod_blog" local ipairs = ipairs local tonumber = tonumber _ENV = {} -- *********************************************************************** -- usage: init() -- desc: Intialize the handler module -- *********************************************************************** function init(conf) conf.blog = blog.init(conf.config) conf.url = url:match(conf.url) return true end -- *********************************************************************** function handler(config,_,loc,pathinfo,ios) if pathinfo == "" then loc.path = loc.path .. "/" ios:write("31 ",uurl.toa(loc),"\r\n") return 31 end if not pathinfo:match "^/" then ios:write("51\r\n") return 51 end pathinfo = pathinfo:sub(2,-1) local first = blog.bookend("first") local last = blog.bookend("last") -- ---------------------------------------------------------------- local function affiliate(location) for _,aff in ipairs(config.blog.affiliate) do if location.scheme == aff.proto then return string.format(aff.link,location.path) end end return uurl.toa(location) end -- ---------------------------------------------------------------- local function xlat_links(when,links) for i,link in ipairs(links) do local u = url:match(link) if u.scheme then local ltxt = affiliate(u) ios:write(string.format("=> %s [%d] %s\n",ltxt,i,ltxt)) else local t = blog.tumbler.new(u.path,first,last) if t then local ltxt = config.url.path .. u.path ios:write(string.format("=> %s [%d] %s\n",ltxt,i,ltxt)) else if u.path:match "^/" then if u.path:find("/thisday",1,true) then local ltxt = config.url.path .. u.path:sub(2,-1) ios:write(string.format("=> %s [%d] %s\n",ltxt,i,ltxt)) else ios:write(string.format("=> %s%s [%d] %s%s\n",config.blog.url,u.path:sub(2,-1),i,config.blog.url,u.path:sub(2,-1))) end else if u.path:find("parallel/",1,true) then ios:write(string.format("=> %s%s [%d] %s%s\n",config.blog.url,u.path,i,config.blog.url,u.path)) else local filename = string.format( "%s%04d/%02d/%02d/%s", config.url.path, when.year, when.month, when.day, u.path ) ios:write(string.format("=> %s [%d] %s\n",filename,i,filename)) end end end end end end -- ---------------------------------------------------------------- local function display(tumbler) local function geminilink(entry) return uurl.toa(uurl.merge(config.url, { path = string.format("%04d/%02d/%02d.%d", entry.when.year, entry.when.month, entry.when.day, entry.when.part) })) end for entry in blog.entries(tumbler) do local x = html(entry.body) local text,links = format(x) ios:write("# ",entry.title,"\n") if tumbler.range then ios:write("## ",os.date("%A, %B %d, %Y",os.time(tumbler.start)),"\n") ios:write("=> ",geminilink(entry),"\n\n") end ios:write(text) xlat_links(tumbler.start,links) ios:write("\n") local gemmention = blog.filename { filename = string.format("%d.webmention",entry.when.part), start = entry.when } local f = io.open(gemmention,"r") if f then -- ----------------------------------------------------------------- -- In order to avoid blocking the entire process, we need to call -- fsys._lock() with the non-blocking option, and if we would lock, -- just yield our coroutine and try again. Yes, this may burn some -- CPU cycles, but it shouldn't be that bad. I can always think on -- this if it does become an issue. -- ----------------------------------------------------------------- local function lock() local okay,err1 = fsys._lock(f,'read',true) if okay then return okay end if err1 == errno.EACCESS or err1 == errno.EAGAIN then nfl.schedule(coroutine.running()) coroutine.yield() return lock() else return okay,err1 end end local okay,err2 = lock() if not okay then syslog('error',"%s: %s",gemmention,errno[err2]) ios:write("40 Temporary failure\r\n") return 40 end ios:write("### Discussions about this entry\n\n") -- ------------------------------------------------------------------- -- I'm accumulating data into a table instead of writing it out -- directly to the output stream because I don't want to get into a -- deadlock with the locked file. The blocking will happen at the OS -- level, not the framework level, so I have to be careful here. -- ------------------------------------------------------------------- local res = {} for line in f:lines() do local u,t = line:match("^(%S+)%s+(.*)") if not u then u = line:match("^(%S+)") t = u end if t == "" then t = u end table.insert(res,string.format("=> %s %s",u,t)) end fsys._lock(f,'release',true) f:close() ios:write(table.concat(res,'\n'),'\n\n') end ios:write("=> /gemmention?gemini-mention Gemini Mention this post\n") ios:write(string.format("=> mailto:%s Contact the author\n\n",blog.CONF.author.email)) end return 20 end -- ---------------------------------------------------------------- local function thisday(req) local month,day = req:match("^thisday/?(%d+)/(%d+)") if not month then local now = os.date("*t") month = now.month day = now.day else month = tonumber(month) day = tonumber(day) end local bytes = ios.__wbytes for year = first.year , last.year do local x = string.format("%04d/%02d/%02d",year,month,day) local tumbler = blog.tumbler.new(x,first,last) if tumbler then tumbler.range = true display(tumbler) end end if ios.__wbytes == bytes then ios:write("There are no entries for this date.\n") end return 20 end -- ---------------------------------------------------------------- -- If the request is empty, display the year menu -- ---------------------------------------------------------------- if pathinfo == "" then ios:write("20 text/gemini\r\n") for i = last.year , first.year , -1 do ios:write(string.format("=> %s%04d %04d\n",config.url.path,i,i)) end return 20 end -- ---------------------------------------------------------------- -- There's a tumbler. Parse and handle -- ---------------------------------------------------------------- local tumbler = blog.tumbler.new(pathinfo,first,last) if not tumbler then if pathinfo:match "^thisday" then ios:write("20 text/gemini\r\n") return thisday(pathinfo) else ios:write("51\r\n") return 51 end end if tumbler.redirect then ios:write(string.format("31 %s%s\r\n",config.url.path,blog.tumbler.canonical(tumbler))) return 31 end if tumbler.file and tumbler.filename ~= "" then local filename = blog.filename(tumbler) if filename:match("%.x%-html$") then local f,err = io.open(filename,"rb") if not f then syslog('error',"%s: %s",filename,err) ios:write("51\r\n") return 51 end ios:write("20 text/gemini\r\n") local text,links = format(html(f:read("a"))) ios:write(text) xlat_links(tumbler.start,links) f:close() return 20 else local conf = { file = blog.filename(tumbler) } readfile.init(conf) return readfile.handler(conf,_,_,_,ios) end elseif tumbler.range then ios:write("20 text/gemini\r\n") return display(tumbler) elseif tumbler.ustart == 'year' then local when = { year = tumbler.start.year , month = 1 , day = 1 } ios:write("20 text/gemini\r\n") for i = 1 , 12 do if tumbler.start.year == first.year and i >= first.month or tumbler.start.year == last.year and i <= last.month or tumbler.start.year > first.year and tumbler.start.year < last.year then when.month = i local d = os.time(when) ios:write(string.format("=> %s%04d/%02d %s\n",config.url.path,when.year,i,os.date("%B",d))) end end return 20 elseif tumbler.ustart == 'month' then ios:write("20 text/gemini\r\n") local entries for day = 1 , date.daysinmonth(tumbler.start) do tumbler.start.day = day local list = blog.day_titles(tumbler.start) if #list > 0 then entries = true ios:write(string.format("=> %s%04d/%02d/%02d %04d/%02d/%02d\n",config.url.path,tumbler.start.year,tumbler.start.month,day,tumbler.start.year,tumbler.start.month,tumbler.start.day)) for i,title in ipairs(list) do ios:write(string.format("=> %s%04d/%02d/%02d.%d %s\n",config.url.path,tumbler.start.year,tumbler.start.month,day,i,title)) end end end if not entries then ios:write("There are no entries this month\n") end return 20 elseif tumbler.ustart == 'day' then ios:write("20 text/gemini\r\n") local list = blog.day_titles(tumbler.start) for i,title in ipairs(list) do ios:write(string.format("=> %s%04d/%02d/%02d.%d %s\n",config.url.path,tumbler.start.year,tumbler.start.month,tumbler.start.day,i,title)) end return 20 else ios:write("20 text/gemini\r\n") return display(tumbler) end end -- *********************************************************************** return _ENV