mirror of
https://github.com/ntop/ntopng.git
synced 2026-08-23 23:33:30 +00:00
289 lines
9.1 KiB
Lua
289 lines
9.1 KiB
Lua
--
|
|
-- (C) 2013-26 - ntop.org
|
|
--
|
|
|
|
local dirs = ntop.getDirs()
|
|
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path
|
|
|
|
local json = require "dkjson"
|
|
local os_utils = require "os_utils"
|
|
|
|
local demo_utils = {}
|
|
|
|
local DEMO_CONFIG_DIR = os_utils.fixPath(dirs.installdir .. "/httpdocs/demo_config")
|
|
local REGISTRY_FILE = os_utils.fixPath(DEMO_CONFIG_DIR .. "/demo_registry.json")
|
|
local REGISTRY_FILENAME = "demo_registry.json"
|
|
|
|
-- Default anti-spam grace period before a tour is allowed to show, per
|
|
-- trigger type.
|
|
local DEFAULT_DELAY_SECONDS = {
|
|
first_login = 0, -- 2 minutes: let them get their bearings first
|
|
new_feature = 20,
|
|
}
|
|
|
|
-- Named visibility conditions a demo can require via "visibility_condition"
|
|
-- in demo_registry.json (or the demo's own JSON file).
|
|
local VISIBILITY_CONDITIONS = {
|
|
has_nanalyst = function()
|
|
return ntop.hasnAnalyst and ntop.hasnAnalyst() or false
|
|
end,
|
|
}
|
|
|
|
-- ##############################################
|
|
|
|
local function read_json_file(path)
|
|
local f = io.open(path, "r")
|
|
if f == nil then
|
|
return nil
|
|
end
|
|
|
|
local content = f:read("*all")
|
|
f:close()
|
|
|
|
local decoded = json.decode(content)
|
|
return decoded
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Auto-discovers every demo JSON file in httpdocs/demo_config/ (any *.json
|
|
-- except demo_registry.json itself) and registers it using its own top-level
|
|
-- id/title/trigger/version
|
|
function demo_utils.get_registry()
|
|
local manual = read_json_file(REGISTRY_FILE) or { demos = {} }
|
|
local manual_by_id = {}
|
|
for _, d in ipairs(manual.demos or {}) do
|
|
manual_by_id[d.id] = d
|
|
end
|
|
|
|
local demos = {}
|
|
local seen_ids = {}
|
|
local files = ntop.readdir(DEMO_CONFIG_DIR) or {}
|
|
|
|
for fname in pairs(files) do
|
|
if fname:match("%.json$") and fname ~= REGISTRY_FILENAME then
|
|
local config = read_json_file(os_utils.fixPath(DEMO_CONFIG_DIR .. "/" .. fname))
|
|
|
|
if config and config.id and not seen_ids[config.id] then
|
|
seen_ids[config.id] = true
|
|
local override = manual_by_id[config.id] or {}
|
|
|
|
demos[#demos + 1] = {
|
|
id = config.id,
|
|
file = fname,
|
|
title = override.title or config.title,
|
|
description = override.description,
|
|
trigger = override.trigger or config.trigger,
|
|
delay_seconds = override.delay_seconds
|
|
or DEFAULT_DELAY_SECONDS[override.trigger or config.trigger] or 0,
|
|
enabled = (override.enabled ~= false),
|
|
visibility_condition = override.visibility_condition or config.visibility_condition,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Manual entries whose file wasn't found by the scan (e.g. bad filename)
|
|
-- still surface, so a misconfiguration is visible via the status list
|
|
-- rather than silently disappearing.
|
|
for id, d in pairs(manual_by_id) do
|
|
if not seen_ids[id] then
|
|
demos[#demos + 1] = {
|
|
id = d.id,
|
|
file = d.file,
|
|
title = d.title,
|
|
description = d.description,
|
|
trigger = d.trigger,
|
|
delay_seconds = d.delay_seconds or DEFAULT_DELAY_SECONDS[d.trigger] or 0,
|
|
enabled = (d.enabled ~= false),
|
|
visibility_condition = d.visibility_condition,
|
|
}
|
|
end
|
|
end
|
|
|
|
return { demos = demos }
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Evaluates a demo's "visibility_condition" (nil/unknown name => always visible)
|
|
function demo_utils.is_visible(demo)
|
|
if isEmptyString(demo.visibility_condition) then
|
|
return true
|
|
end
|
|
|
|
local check = VISIBILITY_CONDITIONS[demo.visibility_condition]
|
|
return check == nil or check() == true
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Returns the full step config for a given demo id, nil if not found/disabled
|
|
function demo_utils.get_demo_config(demo_id)
|
|
local registry = demo_utils.get_registry()
|
|
|
|
for _, demo in ipairs(registry.demos or {}) do
|
|
if demo.id == demo_id and demo.enabled ~= false and demo.file and demo_utils.is_visible(demo) then
|
|
local path = os_utils.fixPath(DEMO_CONFIG_DIR .. "/" .. demo.file)
|
|
local config = read_json_file(path)
|
|
|
|
if config then
|
|
-- merge registry metadata (title/trigger) into the config response
|
|
config.trigger = config.trigger or demo.trigger
|
|
config.title = config.title or demo.title
|
|
end
|
|
|
|
return config
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function progress_key(username, demo_id)
|
|
return string.format("ntopng.user.%s.demo_tour.%s", username, demo_id)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Returns the progress table for a demo/user, or nil if never started
|
|
function demo_utils.get_progress(username, demo_id)
|
|
local raw = ntop.getCache(progress_key(username, demo_id))
|
|
|
|
if isEmptyString(raw) then
|
|
return nil
|
|
end
|
|
|
|
return json.decode(raw)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Persists a progress table for a demo/user
|
|
function demo_utils.save_progress(username, demo_id, progress)
|
|
ntop.setCache(progress_key(username, demo_id), json.encode(progress))
|
|
return true
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Records the current step index reached by the user
|
|
function demo_utils.set_step(username, demo_id, step_index)
|
|
local progress = demo_utils.get_progress(username, demo_id) or {}
|
|
|
|
progress.step_index = step_index
|
|
progress.updated_at = os.time()
|
|
|
|
return demo_utils.save_progress(username, demo_id, progress)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Marks a demo as fully completed. `version` is the config version the user
|
|
-- just finished, so a later version bump can re-trigger the tour.
|
|
function demo_utils.mark_complete(username, demo_id, version)
|
|
local progress = demo_utils.get_progress(username, demo_id) or {}
|
|
|
|
progress.completed_at = os.time()
|
|
progress.seen_version = version
|
|
progress.dismissed = false
|
|
|
|
return demo_utils.save_progress(username, demo_id, progress)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Marks a demo as dismissed ("never show again")
|
|
function demo_utils.dismiss(username, demo_id)
|
|
local progress = demo_utils.get_progress(username, demo_id) or {}
|
|
|
|
progress.dismissed = true
|
|
progress.dismissed_at = os.time()
|
|
|
|
return demo_utils.save_progress(username, demo_id, progress)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Decides whether a demo should be shown to this user right now.
|
|
-- Re-shows automatically when the JSON config `version` is bumped.
|
|
-- `delay_seconds` is an anti-spam grace period: the first time a user is
|
|
-- ever eligible for this demo, we just record the timestamp and say no —
|
|
-- nothing pops up on the very first page they land on.
|
|
function demo_utils.should_show(username, demo_id, demo_version, delay_seconds)
|
|
delay_seconds = delay_seconds or 0
|
|
local progress = demo_utils.get_progress(username, demo_id)
|
|
|
|
if progress == nil then
|
|
demo_utils.save_progress(username, demo_id, { first_seen_at = os.time() })
|
|
return delay_seconds <= 0
|
|
end
|
|
|
|
if progress.dismissed then
|
|
return false
|
|
end
|
|
|
|
if progress.completed_at ~= nil then
|
|
if demo_version and (progress.seen_version == nil or progress.seen_version < demo_version) then
|
|
return true -- version bump: re-show immediately, user is already engaged
|
|
end
|
|
return false
|
|
end
|
|
|
|
if progress.first_seen_at == nil then
|
|
progress.first_seen_at = os.time()
|
|
demo_utils.save_progress(username, demo_id, progress)
|
|
return delay_seconds <= 0
|
|
end
|
|
|
|
return (os.time() - progress.first_seen_at) >= delay_seconds
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Seeds first_seen_at for every existing user x registered demo that has no
|
|
-- progress key yet. Run once at ntopng startup so users who existed before a
|
|
-- demo was added (fresh install upgrade)
|
|
function demo_utils.seed_missing_progress()
|
|
local registry = demo_utils.get_registry()
|
|
local users = ntop.getUsers and ntop.getUsers() or {}
|
|
|
|
for username, _ in pairs(users) do
|
|
for _, demo in ipairs(registry.demos or {}) do
|
|
if demo.enabled ~= false and demo_utils.get_progress(username, demo.id) == nil then
|
|
demo_utils.save_progress(username, demo.id, { first_seen_at = os.time() })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Convenience: full status list (should_show flag) for every registered demo
|
|
function demo_utils.get_status_for_user(username)
|
|
local registry = demo_utils.get_registry()
|
|
local out = {}
|
|
|
|
for _, demo in ipairs(registry.demos or {}) do
|
|
if demo.enabled ~= false and demo_utils.is_visible(demo) then
|
|
local config = demo_utils.get_demo_config(demo.id)
|
|
local version = config and config.version or 1
|
|
|
|
out[#out + 1] = {
|
|
id = demo.id,
|
|
title = demo.title,
|
|
trigger = demo.trigger,
|
|
version = version,
|
|
should_show = demo_utils.should_show(username, demo.id, version, demo.delay_seconds),
|
|
}
|
|
end
|
|
end
|
|
|
|
return out
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
return demo_utils
|