💾 Archived View for gemini.conman.org › extensions › port70 › handlers › blog.lua captured on 2020-10-31 at 23:59:38.

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 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