Software / code / prosody
Comparison
plugins/mod_account_activity.lua @ 13665:30a91d819913 13.0
mod_account_activity: Record an account's last activity timestamp
This is similar to mod_lastlog/mod_lastlog2.
Some functionality was dropped, compared to mod_lastlog2. These features
(recording the IP address, or tracking the timestamp of multiple events) are
handled better by the mod_audit family of modules. For example, those
correctly handle multiple logins, IP address truncation, and data retention
policies.
The "registered" timestamp from mod_lastlog2 was also dropped, as this has
been stored in account_details by Prosody itself since at least 0.12 already.
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Wed, 12 Feb 2025 12:33:45 +0000 |
| child | 13667:3052eae50c98 |
comparison
equal
deleted
inserted
replaced
| 13664:5528bc5ab019 | 13665:30a91d819913 |
|---|---|
| 1 local jid = require "util.jid"; | |
| 2 local time = os.time; | |
| 3 | |
| 4 local store = module:open_store(nil, "keyval+"); | |
| 5 | |
| 6 module:hook("authentication-success", function(event) | |
| 7 local session = event.session; | |
| 8 if session.username then | |
| 9 store:set_key(session.username, "timestamp", time()); | |
| 10 end | |
| 11 end); | |
| 12 | |
| 13 module:hook("resource-unbind", function(event) | |
| 14 local session = event.session; | |
| 15 if session.username then | |
| 16 store:set_key(session.username, "timestamp", time()); | |
| 17 end | |
| 18 end); | |
| 19 | |
| 20 local user_sessions = prosody.hosts[module.host].sessions; | |
| 21 function get_last_active(username) --luacheck: ignore 131/get_last_active | |
| 22 if user_sessions[username] then | |
| 23 return os.time(), true; -- Currently connected | |
| 24 else | |
| 25 local last_activity = store:get(username); | |
| 26 if not last_activity then return nil; end | |
| 27 return last_activity.timestamp; | |
| 28 end | |
| 29 end | |
| 30 | |
| 31 module:add_item("shell-command", { | |
| 32 section = "user"; | |
| 33 section_desc = "View user activity data"; | |
| 34 name = "activity"; | |
| 35 desc = "View the last recorded user activity for an account"; | |
| 36 args = { { name = "jid"; type = "string" } }; | |
| 37 host_selector = "jid"; | |
| 38 handler = function(self, userjid) --luacheck: ignore 212/self | |
| 39 local username = jid.prepped_split(userjid); | |
| 40 local last_timestamp, is_online = get_last_active(username); | |
| 41 if not last_timestamp then | |
| 42 return true, "No activity"; | |
| 43 end | |
| 44 | |
| 45 return true, ("%s (%s)"):format(os.date("%Y-%m-%d %H:%M:%S", last_timestamp), (is_online and "online" or "offline")); | |
| 46 end; | |
| 47 }); | |
| 48 | |
| 49 module:add_item("shell-command", { | |
| 50 section = "migrate"; | |
| 51 section_desc = "Perform data migrations"; | |
| 52 name = "account_activity_lastlog2"; | |
| 53 desc = "Migrate account activity information from mod_lastlog2"; | |
| 54 args = { { name = "host"; type = "string" } }; | |
| 55 host_selector = "host"; | |
| 56 handler = function(self, host) --luacheck: ignore 212/host | |
| 57 local lastlog2 = module:open_store("lastlog2", "keyval+"); | |
| 58 local n_updated, n_errors, n_skipped = 0, 0, 0; | |
| 59 | |
| 60 local async = require "util.async"; | |
| 61 | |
| 62 local p = require "util.promise".new(function (resolve) | |
| 63 local async_runner = async.runner(function () | |
| 64 local n = 0; | |
| 65 for username in lastlog2:items() do | |
| 66 n = n + 1; | |
| 67 if n % 100 == 0 then | |
| 68 self.session.print(("Processed %d..."):format(n)); | |
| 69 async.sleep(0); | |
| 70 end | |
| 71 local lastlog2_data = lastlog2:get(username); | |
| 72 if lastlog2_data then | |
| 73 local current_data, err = store:get(username); | |
| 74 if not current_data then | |
| 75 if not err then | |
| 76 current_data = {}; | |
| 77 else | |
| 78 n_errors = n_errors + 1; | |
| 79 end | |
| 80 end | |
| 81 if current_data then | |
| 82 local imported_timestamp = current_data.timestamp; | |
| 83 local latest; | |
| 84 for k, v in pairs(lastlog2_data) do | |
| 85 if k ~= "registered" and (not latest or v.timestamp > latest) then | |
| 86 latest = v.timestamp; | |
| 87 end | |
| 88 end | |
| 89 if latest and (not imported_timestamp or imported_timestamp < latest) then | |
| 90 local ok, err = store:set_key(username, "timestamp", latest); | |
| 91 if ok then | |
| 92 n_updated = n_updated + 1; | |
| 93 else | |
| 94 self.session.print(("WW: Failed to import %q: %s"):format(username, err)); | |
| 95 n_errors = n_errors + 1; | |
| 96 end | |
| 97 else | |
| 98 n_skipped = n_skipped + 1; | |
| 99 end | |
| 100 end | |
| 101 end | |
| 102 end | |
| 103 return resolve(("%d accounts imported, %d errors, %d skipped"):format(n_updated, n_errors, n_skipped)); | |
| 104 end); | |
| 105 async_runner:run(true); | |
| 106 end); | |
| 107 return p; | |
| 108 end; | |
| 109 }); |