💾 Archived View for gemini.conman.org › extensions › port70 › handlers › blog.lua captured on 2024-12-17 at 12:38:54.

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 last_link
-- luacheck: ignore 611 631

local errno    = require "org.conman.errno"
local nfl      = require "org.conman.nfl"
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 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 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.mod_blog.html"
local uurl     = require "org.conman.app.port70.handlers.blog.url-util"
local format   = require "org.conman.app.port70.handlers.blog.format"
local blog     = require "org.conman.app.mod_blog"

local require  = require
local ipairs   = ipairs
local tonumber = tonumber

_ENV = {}

-- ***********************************************************************
-- usage:       init()
-- desc:        Intialize the handler module
-- ***********************************************************************

function init(conf)
  conf.blog = blog.init(conf.config)
  return true
end

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

function handler(config,request,ios)
  local CONF  = require "port70.CONF"
  local first = blog.bookend("first")
  local last  = blog.bookend("last")
  local port do
    if CONF.network.port == 70 or CONF.network.port == 'gopher' then
      port = ""
    else
      port = string.format(":%d",CONF.network.port)
    end
  end
  
  -- ----------------------------------------------------------------
  
  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)
    local function filetype(filename)
      if fsys.access(filename,"r") then
        local mime = mimetype:match(magic(filename))
        local ftype
        
        if not mime then
          ftype = gtypes.binary
        else
          if mime.type:find("text/",1,true) then
            ftype = gtypes.file
          elseif mime.type:find("image/gif",1,true) then
            ftype = gtypes.gif
          elseif mime.type:find("image/",1,true) then
            ftype = gtypes.image
          elseif mime.type:find("audio/",1,true) then
            ftype = gtypes.sound
          elseif mime.type:find("video/",1,true) then
            ftype = gtypes.video
          else
            ftype = gtypes.binary
          end
        end
        return ftype
      end
      
      return gtypes.file
    end
    
    -- ----------------------------------------------------------------
    
    for i,link in ipairs(links) do
      local u = url:match(link)
      
      if u.scheme then
        ios:write(string.format("[%d] %s\n",i,affiliate(u)))
        
      else
        local t = blog.tumbler.new(u.path,first,last)
        if t then
          if t.ustart == 'part' or t.range
          or t.file and t.filename ~= "" then
            if t.file then
              local filename = config.blog.basedir .. string.format("/%04d/%02d/%02d/%s",t.start.year,t.start.month,t.start.day,t.filename)
              ios:write(string.format("[%d] gopher://%s%s/%s%s%s\n",
                        i,
                        CONF.network.host,
                        port,
                        filetype(filename),
                        config.selector,
                        u.path
                ))
            else
              ios:write(string.format("[%d] gopher://%s%s/0%s%s\n",
                        i,
                        CONF.network.host,
                        port,
                        config.selector,
                        u.path
              ))
            end
          else
            ios:write(string.format("[%d] gopher://%s%s/1%s%s\n",
                        i,
                        CONF.network.host,
                        port,
                        config.selector,
                        u.path
            ))
          end
        else
          if u.path:match "^/" then
            if u.path:find("/thisday",1,true) then
              ios:write(string.format("[%d] gopher://%s%s/0%s%s\n",
                        i,
                        CONF.network.host,
                        port,
                        config.selector,
                        u.path:sub(2,-1)
              ))
            else
              ios:write(string.format("[%d] %s%s\n",i,config.blog.url,u.path:sub(2,-1)))
            end
          else
            if u.path:find("parallel/",1,true) then
              ios:write(string.format("[%d] %s%s\n",i,config.blog.url,u.path))
            else
              local filename = string.format(
                "%s/%04d/%02d/%02d/%s",
                config.blog.basedir,
                when.year,
                when.month,
                when.day,
                u.path
              )
              ios:write(string.format("[%d] gopher://%s%s/%s%s%04d/%02d/%02d/%s\n",
                        i,
                        CONF.network.host,
                        port,
                        filetype(filename),
                        config.selector,
                        when.year,
                        when.month,
                        when.day,
                        u.path
              ))
            end
          end
        end
      end
    end
  end
  
  -- ----------------------------------------------------------------
  
  local function display(tumbler)
    local function gopherlink(entry)
      return string.format("gopher://%s%s/%s%s%04d/%02d/%02d.%d",
                CONF.network.host,
                port,
                gtypes.file,
                config.selector,
                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)
      if x then
        local text,links = format(x)
        local t = wrapt(entry.title,77)
        table.insert(t,1,"")
        table.insert(t,2,"* * * * *")
        table.insert(t,3,"")
        table.insert(t,"")
        if tumbler.range then
          table.insert(t,os.date("%A, %B %d, %Y",os.time(tumbler.start)))
          table.insert(t,gopherlink(entry))
          table.insert(t,"")
        end
        
        for i = 1 , #t do
          t[i] = string.rep(" ",(80 - utf8.len(t[i])) // 2) .. t[i]
        end
        
        ios:write(table.concat(t,"\n"),'\n')
        ios:write(text)
        xlat_links(tumbler.start,links)
        
        local webmention = blog.filename {
                filename = string.format("%d.webmention",entry.when.part),
                start    = entry.when
        }
        local f = io.open(webmention,"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.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",webmention,errno[err2])
            return true
          end
          
          ios:write("---\n\nDiscussions about this page\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,title = line:match("^(%S+)%s*(.*)")
            if not u then
              u = line:match("^(%S+)")
              title = u
            end
            if title == "" then
              title = u
            end
            
            table.insert(res,title)
            table.insert(res,"  " .. u)
            table.insert(res,"")
          end
          fsys._lock(f,'release',true)
          f:close()
          ios:write(table.concat(res,'\n'))
        end
        
        ios:write(string.format("\nEmail author at %s\n\n",blog.CONF.author.email))
        
        -- ios:write("how to do mentions for gopher?\n")
      else
        syslog(
                'error',
                "bad entry: %4d/%02d/%02d.%d",
                entry.when.year,
                entry.when.month,
                entry.when.day,
                entry.when.part
        )
      end
    end
    return true
  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
    
    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 == 0 then
      ios:write("There are no entries for this date.\n")
    end
    
    return true
  end
  
  -- ----------------------------------------------------------------
  -- If the request is empty, display the year menu
  -- ----------------------------------------------------------------
  
  if request.rest == "" then
    for i = last.year , first.year , -1 do
      local itop = string.format("%04d",i)
      ios:write(mklink {
        type     = 'dir',
        display  = itop,
        selector = config.selector .. itop,
      })
    end
    return true
  end
  
  -- ----------------------------------------------------------------
  -- There's a tumbler.  Parse and handle
  -- ----------------------------------------------------------------
  
  local tumbler = blog.tumbler.new(request.rest,first,last)
  if not tumbler then
    if request.rest:match "^thisday" then
      return thisday(request.rest)
    else
      ios:write(mklink {
                type = 'error',
                display = 'Selector not found',
                selector = request.rest
        })
      return false
    end
  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(mklink {
          type = 'error',
          display = 'Selector not found',
          selector = request
        })
        return false
      end
      
      local text,links = format(html(f:read("a")))
      ios:write(text)
      xlat_links(tumbler.start,links)
      f:close()
      return true
    else
      return readfile(blog.filename(tumbler),"\1",config,request,ios)
    end
    
  elseif tumbler.range then
    return display(tumbler)
    
  elseif tumbler.ustart == 'year' then
    local when = { year = 2021 , month = 1 , day = 1 }
    
    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(mklink {
                type = 'dir',
                display = os.date("%B",d),
                selector = string.format("%s%04d/%02d",config.selector,tumbler.start.year,i)
        })
      end
    end
    return true
    
  elseif tumbler.ustart == 'month' then
    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(mklink {
                type     = 'dir',
                display  = string.format("%04d/%02d/%02d",tumbler.start.year,tumbler.start.month,tumbler.start.day),
                selector = string.format("%s%04d/%02d/%02d",config.selector,tumbler.start.year,tumbler.start.month,tumbler.start.day),
        })
        
        for i,title in ipairs(list) do
          ios:write(mklink {
            type     = 'file',
            display  = title,
            selector = string.format("%s%04d/%02d/%02d.%d",config.selector,tumbler.start.year,tumbler.start.month,tumbler.start.day,i),
          })
        end
      end
    end
    
    if not entries then
      ios:write(mklink {
        type = 'info',
        display = "There are no entries this month"
      })
    end
    return true
    
  elseif tumbler.ustart == 'day' then
    local list = blog.day_titles(tumbler.start)
    for i,title in ipairs(list) do
      ios:write(mklink {
        type     = 'file',
        display  = title,
        selector = string.format("%s%04d/%02d/%02d.%d",config.selector,tumbler.start.year,tumbler.start.month,tumbler.start.day,i),
      })
    end
    return true
    
  else
    return display(tumbler)
  end
end

-- ***********************************************************************
-- usage:       link = last_link()
-- desc:        return a gopher link for the latest blog entry
-- return:      link (string) gopher link
-- ***********************************************************************

function last_link(selector)
  local last = blog.bookend("last")
  return string.format(
                "%s%d/%02d/%02d.%d",
                selector,
                last.year,
                last.month,
                last.day,
                last.part
        )
end

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

return _ENV