💾 Archived View for gemini.conman.org › extensions › GLV-1 › handlers › blog.lua captured on 2023-07-10 at 17:37:46.

View Raw

More Information

⬅️ Previous capture (2023-01-29)

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

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