Ctrl K
ComunidadGuías…

extensions/web-base/web-base.lua

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
local extension = ...

-- -config.extensions.web-base.assets ../../assets/www_static/

local class = require('jls.lang.class')
local logger = extension:getLogger()
local event = require('jls.lang.event')
local File = require('jls.io.File')
local HttpExchange = require('jls.net.http.HttpExchange')
local HttpHandler = require('jls.net.http.HttpHandler')
local FileHttpHandler = require('jls.net.http.handler.FileHttpHandler')
local ZipFileHttpHandler = require('jls.net.http.handler.ZipFileHttpHandler')
local HttpContext = require('jls.net.http.HttpContext')
local WebSocketUpgradeHandler = require('jls.net.http.WebSocket').UpgradeHandler
local json = require("jls.util.json")
local Map = require("jls.util.Map")
local List = require("jls.util.List")
local tables = require("jls.util.tables")

local utils = require('lha.utils')

local Logger = logger:getClass()
local recordLog = Logger.getLogRecorder()

local AddonFileHttpHandler = class.create(FileHttpHandler, function(fileHttpHandler)
  function fileHttpHandler:getPath(httpExchange)
    return select(2, httpExchange:getRequestArguments())
  end
end)

local RollingList = class.create(List, function(rollingList)
  function rollingList:initialize(size)
    self.index = size
    self.size = size
  end
  function rollingList:add(value)
    if value == nil then
      error('Cannot add nil value')
    end
    local i = self.index % self.size + 1
    local dropped = self[i]
    self[i] = value
    self.index = i
    if dropped then
      self:onDropped(dropped)
    end
    return self
  end
  function rollingList:onDropped(value)
  end
  function rollingList:values()
    if #self ~= self.size or self.index == self.size then
      return self
    end
    local l = {}
    table.move(self, self.index + 1, self.size, 1, l)
    table.move(self, 1, self.index, 1, l)
    return l
  end
end)

local websockets = {}
local addons = {}

local function cleanup()
  websockets = {}
  addons = {}
  Logger.setLogRecorder(recordLog)
end

local function onWebSocketClose(webSocket)
  logger:fine('WebSocket closed %s', webSocket)
  List.removeFirst(websockets, webSocket)
end

local function webSocketBroadcast(data)
  logger:fine('WebSocket broadcast')
  local message = json.encode(data)
  for _, websocket in ipairs(websockets) do
    websocket:sendTextMessage(message)
  end
end

local function webSocketSend(sessionId, data)
  logger:fine('WebSocket send')
  local message = json.encode(data)
  for _, websocket in ipairs(websockets) do
    if not sessionId or sessionId == websocket.sessionId then
      websocket:sendTextMessage(message)
    end
  end
end

local batchDataChange = true
local dataChangeEvent = nil

local function onDataChange(value, previousValue, path)
  logger:fine('onDataChange() "%s": "%s" %s', path, value, #websockets)
  if #websockets == 0 then
    return
  end
  if batchDataChange then
    if not dataChangeEvent then
      event:setTimeout(function()
        webSocketBroadcast(dataChangeEvent)
        dataChangeEvent = nil
      end)
      dataChangeEvent = {event = 'data-change'}
    end
    tables.setPath(dataChangeEvent, path, value)
  else
    local thingId, propertyName = string.match(path, '^data/([^/]+)/([^/]+)$')
    if thingId then
      webSocketBroadcast({
        event = 'data-change',
        data = {
          [thingId] = {
            [propertyName] = {
              value = value,
              previousValue = previousValue
            }
          }
        }
      })
    end
  end
end

function extension:registerAddon(id, addon)
  addons[id] = addon
  logger:info('Web base add-on "%s" %s', id, addon and 'registered' or 'unregistered')
  webSocketBroadcast({event = 'addon-change'})
end

function extension:registerAddonExtension(ext, script)
  self:registerAddon(ext:getId(), {
    handler = AddonFileHttpHandler:new(ext:getDir()),
    script = script or 'init.js'
  })
end

function extension:unregisterAddonExtension(ext)
  self:registerAddon(ext:getId())
end

function extension:getWebBaseInfo()
  return {
    addons = Map.size(addons),
    websockets = #websockets
  }
end

extension:watchPattern('^data/.*', onDataChange)

extension:subscribeEvent('startup', function()
  local configuration = extension:getConfiguration()
  cleanup()
  local maxMessageSize = 1024
  local maxMessageCount = 60
  local maxWebSocketLevel = Logger.LEVEL.FINE
  local warnMessages = RollingList:new(maxMessageCount // 3)
  local logMessages = RollingList:new(maxMessageCount - maxMessageCount // 3)
  function logMessages:onDropped(value)
    if value.level >= logger.WARN then
      warnMessages:add(value)
    end
  end
  local bufferMessages
  Logger.setLogRecorder(function(lgr, time, level, message)
    recordLog(lgr, time, level, message)
    if level <= maxWebSocketLevel then
      -- the websocket stack will emit logs
      return
    end
    if #message > maxMessageSize then
      message = string.sub(message, 1, maxMessageSize - 3)..'...'
    end
    local value = {
      time = time, level = level, message = message
    }
    logMessages:add(value)
    if #websockets == 0 then
      bufferMessages = nil
    else
      if not bufferMessages then
        event:setTimeout(function()
          webSocketBroadcast(bufferMessages)
          bufferMessages = nil
        end)
        bufferMessages = {event = 'logs', logs = {}}
      end
      table.insert(bufferMessages.logs, value)
    end
  end)

  local assets = utils.getAbsoluteFile(configuration.assets or 'assets', extension:getDir())
  local assetsHandler
  if assets:isFile() then
    if string.find(assets:getPathName(), '%.zip$') then
      logger:info('Using assets file "%s"', assets)
      assetsHandler = ZipFileHttpHandler:new(assets)
    else
      logger:warn('Invalid assets file "%s"', assets)
      assetsHandler = HttpContext.notFoundHandler
    end
  else
    if assets:isDirectory() then
      logger:info('Using assets directory "%s"', assets)
    else
      logger:warn('Missing assets directory "%s"', assets)
    end
    assetsHandler = FileHttpHandler:new(assets):setCacheControl(configuration.cache)
  end
  local wwwDir = File:new(extension:getDir(), 'www')

  extension:addContext('/(.*)', FileHttpHandler:new(wwwDir, 'r', 'app.html'))
  extension:addContext('/static/(.*)', assetsHandler)
  extension:addContext('/addon/([^/]*)/?(.*)', HttpHandler:new(function(self, exchange)
    local name, path = exchange:getRequestArguments()
    logger:fine('add-on handler "%s" / "%s"', name, path)
    if name == '' then
      local list = {}
      for id, addon in pairs(addons) do
        table.insert(list, {
          id = id,
          script = addon.script
        })
      end
      HttpExchange.ok(exchange, json.encode(list), 'application/json')
    else
      local addon = addons[name]
      if addon and addon.handler then
        logger:fine('calling add-on "%s" handler', name)
        return addon.handler:handle(exchange)
      end
      HttpExchange.notFound(exchange)
    end
  end))
  extension:addContext('/ws/', Map.assign(WebSocketUpgradeHandler:new(), {
    onOpen = function(_, webSocket, exchange)
      logger:fine('WebSocket openned %s', webSocket)
      table.insert(websockets, webSocket)
      webSocket.onClose = onWebSocketClose
      local session = exchange:getSession()
      if session then
        webSocket.sessionId = session:getId()
      end
    end
  }))
  extension:addContext('/logs/', HttpHandler:new(function(self, exchange)
    local l = List.concat({}, logMessages:values(), warnMessages:values())
    HttpExchange.ok(exchange, json.stringify(l), 'application/json')
  end))
  logger:info('WebSocket available on /ws/')

  extension:getEngine():publishEvent('web-base:startup', extension)
end)

extension:subscribeEvent('shutdown', function()
  webSocketBroadcast({event = 'shutdown'})
  cleanup()
end)

extension:subscribeEvent('notification', function(message, sessionId)
  logger:fine('notification: %s', message)
  if type(message) == 'string' then
    webSocketSend(sessionId, {
      event = 'notification',
      message = message
    })
  end
end)