local jid = require "prosody.util.jid"; local time = os.time; local store = module:open_store(nil, "keyval+"); module:hook("authentication-success", function(event) local session = event.session; if session.username then store:set_key(session.username, "timestamp", time()); end end); module:hook("resource-unbind", function(event) local session = event.session; if session.username then store:set_key(session.username, "timestamp", time()); end end); local user_sessions = prosody.hosts[module.host].sessions; function get_last_active(username) --luacheck: ignore 131/get_last_active if user_sessions[username] then return os.time(), true; -- Currently connected else local last_activity = store:get(username); if not last_activity then return nil; end return last_activity.timestamp; end end module:add_item("shell-command", { section = "user"; section_desc = "View user activity data"; name = "activity"; desc = "View the last recorded user activity for an account"; args = { { name = "jid"; type = "string" } }; host_selector = "jid"; handler = function(self, userjid) --luacheck: ignore 212/self local username = jid.prepped_split(userjid); local last_timestamp, is_online = get_last_active(username); if not last_timestamp then return true, "No activity"; end return true, ("%s (%s)"):format(os.date("%Y-%m-%d %H:%M:%S", last_timestamp), (is_online and "online" or "offline")); end; }); module:add_item("shell-command", { section = "user"; section_desc = "View user activity data"; name = "list_inactive"; desc = "List inactive user accounts"; args = { { name = "host"; type = "string" }; { name = "duration"; type = "string" }; }; host_selector = "host"; handler = function(self, host, duration) --luacheck: ignore 212/self local um = require "prosody.core.usermanager"; local duration_sec = require "prosody.util.human.io".parse_duration(duration or ""); if not duration_sec then return false, ("Invalid duration %q - try something like \"30d\""):format(duration); end local now = os.time(); local n_inactive, n_unknown = 0, 0; for username in um.users(host) do local last_active = store:get_key(username, "timestamp"); if not last_active then local created_at = um.get_account_info(username, host).created; if created_at and (now - created_at) > duration_sec then self.session.print(username, ""); n_inactive = n_inactive + 1; elseif not created_at then n_unknown = n_unknown + 1; end elseif (now - last_active) > duration_sec then self.session.print(username, os.date("%Y-%m-%dT%T", last_active)); n_inactive = n_inactive + 1; end end if n_unknown > 0 then return true, ("%d accounts inactive since %s (%d unknown)"):format(n_inactive, os.date("%Y-%m-%dT%T", now - duration_sec), n_unknown); end return true, ("%d accounts inactive since %s"):format(n_inactive, os.date("%Y-%m-%dT%T", now - duration_sec)); end; }); module:add_item("shell-command", { section = "migrate"; section_desc = "Perform data migrations"; name = "account_activity_lastlog2"; desc = "Migrate account activity information from mod_lastlog2"; args = { { name = "host"; type = "string" } }; host_selector = "host"; handler = function(self, host) --luacheck: ignore 212/host local lastlog2 = module:open_store("lastlog2", "keyval+"); local n_updated, n_errors, n_skipped = 0, 0, 0; local async = require "prosody.util.async"; local p = require "prosody.util.promise".new(function (resolve) local async_runner = async.runner(function () local n = 0; for username in lastlog2:items() do n = n + 1; if n % 100 == 0 then self.session.print(("Processed %d..."):format(n)); async.sleep(0); end local lastlog2_data = lastlog2:get(username); if lastlog2_data then local current_data, err = store:get(username); if not current_data then if not err then current_data = {}; else n_errors = n_errors + 1; end end if current_data then local imported_timestamp = current_data.timestamp; local latest; for k, v in pairs(lastlog2_data) do if k ~= "registered" and (not latest or v.timestamp > latest) then latest = v.timestamp; end end if latest and (not imported_timestamp or imported_timestamp < latest) then local ok, err = store:set_key(username, "timestamp", latest); if ok then n_updated = n_updated + 1; else self.session.print(("WW: Failed to import %q: %s"):format(username, err)); n_errors = n_errors + 1; end else n_skipped = n_skipped + 1; end end end end return resolve(("%d accounts imported, %d errors, %d skipped"):format(n_updated, n_errors, n_skipped)); end); async_runner:run(true); end); return p; end; });