💾 Archived View for gemini.conman.org › extensions › GLV-1 › handlers › blog.lua captured on 2020-10-31 at 02:18:43.

View Raw

More Information

➡️ Next capture (2021-12-03)

-=-=-=-=-=-=-

-- ***********************************************************************
--
-- 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 <http://www.gnu.org/licenses/>.
--
-- 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 date      = require "org.conman.date"
local ENTITIES  = require "org.conman.const.entity"
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 readfile  = require "GLV-1.handlers.file"
local MSG       = require "GLV-1.MSG"
local uurl      = require "GLV-1.url-util"
local html      = require "org.conman.app.port70.handlers.blog.html"
local format    = require "org.conman.app.GLV-1.handlers.blog.format"

local loadfile  = loadfile
local tonumber  = tonumber
local ipairs    = ipairs

local blog = { require = setmetatable(
	{},
	{
	  __index = function(s) return s end,
	  __call  = function(s) return s end,
	}
        )
}

_ENV = {}

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(base,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("%s%d/%02d/%02d.%d",base,when.year,when.month,when.day,when.part)
      table.insert(acc,string.format("=> %s %s\n",link,deent:match(title)))
      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(base,when)
  when.day  = 1
  local acc = {}
  local d   = os.time(when)
  table.insert(acc,os.date("%B, %Y\n",d))
  local maxday = date.daysinmonth(when)
  
  for day = 1 , maxday do
    when.day    = day
    local posts = collect_day(base,when)
    
    if #posts > 0 then
      local title = string.format("%d/%02d/%02d",when.year,when.month,when.day)
      local link  = string.format("%s%d/%02d/%02d",base,when.year,when.month,when.day)
      table.insert(acc,string.format("=> %s %s\n",link,title))
      for _,post in ipairs(posts) do
        table.insert(acc,post)
      end
    end
  end

  return 20,'text/gemini',table.concat(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 file    = P"/" * Cg(P(1)^1,"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 redir   = P"/" * Cg(Cc(true),'redirect')
local tumbler = Ct(
                      year * month * day * file
                    + year * month * day * part
                    + year * month * day * redir^-1
                    + year * month * redir^-1
                    + year * redir^-1
                    + Cg(Cc('none'),"unit")
                  ) * 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(base,what,fpath)
  local f = io.open(blog.basedir .. fpath,"r")
  
  if not f then
    return 51,MSG[51],""
  end
  
  local doc = html(f)
  f:close()
  
  local txt  = format(doc,base,what,blog.affiliate)
  return 20,'text/gemini',txt
end

-- ***********************************************************************

function handler(_,_,loc,match)
  local what = tumbler:match(match[2])
  
  if not what then
    return 51,MSG[51],""
  end
  
  if what.redirect then
    loc.path = loc.path:sub(1,-2)
    local u = uurl.toa(loc)
    return 31,u,""
  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,string.format("=> %s%04d\n",match[1],i))
    end
    
    return 20,'text/gemini',table.concat(years)
    
  elseif what.unit == 'year' then
    local months = {}
 
    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
        table.insert(months,string.format("=> %s%04d/%02d\n",match[1],what.year,i))
      end
    end
    
    return 20,'text/gemini',table.concat(months)
    
  elseif what.unit == 'month' then
    return collect_month(match[1],what)
    
  elseif what.unit == 'day' then
    return 20,'text/gemini',table.concat(collect_day(match[1],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 status,mime,data = display_part(match[1],what,fpath)
      
      if status ~= 20 then
        return status,mime,data
      else
        return status,mime,'# ' .. titles[what.part] .. '\n\n' .. data
      end
    else
      return 51,MSG[51],""
    end
    
  elseif what.unit == 'file' then
    local fpath = string.format("/%04d/%02d/%02d/%s",what.year,what.month,what.day,what.file)
    if fpath:match("%.x%-html$") then
      return display_part(match[1],what,fpath)
    else
      local conf = { file = blog.basedir .. fpath }
      readfile.init(conf)
      return readfile.handler(conf)
    end
  else
    syslog('error',"Um ... what now?")
    return 51,MSG[51],""
  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
  
  f()
  
  blog._first = read_date(".first")
  
  return true
end

-- ***********************************************************************

return _ENV