Software / code / prosody-modules
Comparison
mod_http_presence/mod_http_presence.lua @ 6344:eb834f754f57 draft default tip
Merge update
| author | Trần H. Trung <xmpp:trần.h.trung@trung.fun> |
|---|---|
| date | Fri, 18 Jul 2025 20:45:38 +0700 |
| parent | 6332:9dcdb56f75dd |
comparison
equal
deleted
inserted
replaced
| 6309:342f88e8d522 | 6344:eb834f754f57 |
|---|---|
| 1 local mod_pep = module:depends("pep"); | |
| 2 module:depends("http"); | |
| 3 | |
| 4 local storagemanager = require "core.storagemanager"; | |
| 5 local usermanager = require "core.usermanager"; | |
| 6 local stanza = require "util.stanza".stanza; | |
| 7 local deserialize = require "util.stanza".deserialize; | |
| 8 local base64_decode = require "util.encodings".base64.decode; | |
| 9 local base64_encode = require "util.encodings".base64.encode; | |
| 10 local http = require "net.http"; | |
| 11 local jid = require "util.jid"; | |
| 12 | |
| 13 function get_user_presence(bare_jid) | |
| 14 local host = jid.host(bare_jid); | |
| 15 local sessions = prosody.hosts[host] and prosody.hosts[host].sessions[jid.node(bare_jid)]; | |
| 16 if not sessions then | |
| 17 return { status = "offline", message = nil }; | |
| 18 end | |
| 19 | |
| 20 local highest_priority_session = nil; | |
| 21 local highest_priority = -math.huge; | |
| 22 | |
| 23 for resource, session in pairs(sessions.sessions) do | |
| 24 if session.presence then | |
| 25 local priority = session.priority or 0; | |
| 26 if priority > highest_priority then | |
| 27 highest_priority = priority; | |
| 28 highest_priority_session = session; | |
| 29 end | |
| 30 end | |
| 31 end | |
| 32 | |
| 33 if not highest_priority_session then | |
| 34 return { status = "offline", message = nil }; | |
| 35 end | |
| 36 | |
| 37 local presence = highest_priority_session.presence; | |
| 38 return { | |
| 39 status = presence and (presence:get_child("show") and presence:get_child("show"):get_text() or "online") or "offline", | |
| 40 message = presence and presence:get_child("status") and presence:get_child("status"):get_text() or nil | |
| 41 }; | |
| 42 end | |
| 43 | |
| 44 function get_user_avatar(bare_jid) | |
| 45 local pep_service = mod_pep.get_pep_service(jid.node(bare_jid)); | |
| 46 if not pep_service then | |
| 47 module:log("error", "PEP storage not available"); | |
| 48 return nil; | |
| 49 end | |
| 50 | |
| 51 local meta_ok, hash, meta = pep_service:get_last_item("urn:xmpp:avatar:metadata", module.host); | |
| 52 if not meta_ok or not hash then | |
| 53 module:log("debug", "Failed to get avatar metadata for %s: %s", bare_jid, "Not OK"); | |
| 54 return nil; | |
| 55 end | |
| 56 | |
| 57 local data_ok, data_hash, data = pep_service:get_last_item("urn:xmpp:avatar:data", module.host, hash); | |
| 58 local data_err = nil; | |
| 59 if not data_ok then | |
| 60 data_err = "Not OK"; | |
| 61 elseif data_hash ~= hash then | |
| 62 data_err = "Hash does not match"; | |
| 63 elseif type(data) ~= "table" then | |
| 64 data_err = "Data of type table"; | |
| 65 end | |
| 66 if data_err then | |
| 67 module:log("debug", "Failed to get avatar data for %s, hash %s: %s", bare_jid, hash, data_err); | |
| 68 return nil; | |
| 69 end | |
| 70 local info = meta.tags[1]:get_child("info"); | |
| 71 if not info then | |
| 72 module:log("debug", "Missing avatar info for %s, hash %s", bare_jid, hash); | |
| 73 return nil; | |
| 74 end | |
| 75 return info and info.attr.type or "application/octet-stream", data[1]:get_text(); | |
| 76 end | |
| 77 | |
| 78 function get_user_nickname(bare_jid) | |
| 79 local pep_service = mod_pep.get_pep_service(jid.node(bare_jid)); | |
| 80 if not pep_service then | |
| 81 module:log("error", "PEP storage not available"); | |
| 82 return nil; | |
| 83 end | |
| 84 | |
| 85 local ok, nick, nick_item = pep_service:get_last_item("urn:xmpp:vcard4", module.host); | |
| 86 if not ok then | |
| 87 module:log("debug", "Failed to get nick for %s: %s", bare_jid, "Not OK"); | |
| 88 return nil; | |
| 89 end | |
| 90 | |
| 91 if nick_item and nick_item.tags and nick_item.tags[1] and nick_item.tags[1].tags then | |
| 92 for _, tag in ipairs(nick_item.tags[1].tags) do | |
| 93 if tag.name == "nickname" and tag.tags and tag.tags[1] and tag.tags[1][1] then | |
| 94 nickname = tag.tags[1][1]; | |
| 95 module:log("debug", "Nickname found for JID %s: %s", bare_jid, nickname); | |
| 96 return nickname; | |
| 97 end | |
| 98 end | |
| 99 else | |
| 100 module:log("debug", "Invalid vCard4 item structure for JID %s", bare_jid); | |
| 101 return nil; | |
| 102 end | |
| 103 | |
| 104 module:log("debug", "No <nickname> element in vCard4 for JID %s", bare_jid); | |
| 105 return jid.node(bare_jid); | |
| 106 end | |
| 107 | |
| 108 function get_muc_avatar(bare_jid) | |
| 109 local node = jid.node(bare_jid); | |
| 110 local vcard_store = storagemanager.open(module.host, "vcard_muc") | |
| 111 if not vcard_store then | |
| 112 module:log("error", "MUC vCard store not available for host: %s", module.host); | |
| 113 return nil, nil, "MUC vCard store not available"; | |
| 114 end | |
| 115 | |
| 116 local vcard_data, err = vcard_store:get(node); | |
| 117 if not vcard_data then | |
| 118 module:log("debug", "No vCard data for MUC %s: %s", bare_jid, err or "No data"); | |
| 119 return nil, nil, err or "No vCard data"; | |
| 120 end | |
| 121 | |
| 122 local vcard = deserialize(vcard_data); | |
| 123 if not vcard then | |
| 124 module:log("debug", "Failed to parse vCard for MUC %s", bare_jid); | |
| 125 return nil, nil, "Failed to parse vCard"; | |
| 126 end | |
| 127 | |
| 128 local photo = vcard:get_child("PHOTO"); | |
| 129 if not photo then | |
| 130 module:log("debug", "No <PHOTO> element in vCard for MUC %s", bare_jid); | |
| 131 return nil, nil, "No photo element"; | |
| 132 end | |
| 133 | |
| 134 local content_type = photo:get_child_text("TYPE") or "application/octet-stream"; | |
| 135 local avatar_data = photo:get_child_text("BINVAL"); | |
| 136 if not avatar_data then | |
| 137 module:log("debug", "No <BINVAL> in <PHOTO> for MUC %s", bare_jid); | |
| 138 return nil, nil, "No avatar data"; | |
| 139 end | |
| 140 | |
| 141 module:log("debug", "MUC avatar found for JID %s: type=%s, data=%s", | |
| 142 bare_jid, content_type, avatar_data:sub(1, 20) .. "..."); | |
| 143 return content_type, avatar_data, nil; | |
| 144 end | |
| 145 | |
| 146 function get_muc_info(bare_jid) | |
| 147 local node = jid.node(bare_jid); | |
| 148 local muc_store = storagemanager.open(module.host, "config"); | |
| 149 if not muc_store then | |
| 150 module:log("error", "MUC config store not available for host: %s", module.host); | |
| 151 return nil, nil, "MUC config store not available"; | |
| 152 end | |
| 153 | |
| 154 local config_data, err = muc_store:get(node); | |
| 155 if not config_data then | |
| 156 module:log("debug", "No config data for JID %s: %s", bare_jid, err or "No data"); | |
| 157 return nil, nil, err or "No config data"; | |
| 158 end | |
| 159 | |
| 160 local muc_name = config_data._data and config_data._data.name; | |
| 161 local muc_description = config_data._data and config_data._data.description; | |
| 162 if not muc_name and not muc_description then | |
| 163 module:log("debug", "No name or description in config for JID %s", bare_jid); | |
| 164 return nil, nil, "No name or description"; | |
| 165 end | |
| 166 | |
| 167 module:log("debug", "MUC info for JID %s: name=%s, desc=%s", bare_jid, muc_name, muc_description); | |
| 168 return muc_name, muc_description, nil; | |
| 169 end | |
| 170 | |
| 171 function get_muc_users(bare_jid) | |
| 172 local component = hosts[module.host]; | |
| 173 if not component then | |
| 174 module:log("error", "No component found for host: %s", module.host); | |
| 175 return nil, "No MUC component found"; | |
| 176 end | |
| 177 local muc = component.modules.muc; | |
| 178 if not muc then | |
| 179 module:log("error", "MUC module not loaded for host: %s", module.host); | |
| 180 return nil, "MUC module not loaded"; | |
| 181 end | |
| 182 local room = muc.get_room_from_jid(bare_jid); | |
| 183 if not room then | |
| 184 module:log("error", "Room %s does not exist", bare_jid); | |
| 185 return nil, "Room does not exist"; | |
| 186 end | |
| 187 local count = 0; | |
| 188 for _ in room:each_occupant() do | |
| 189 count = count + 1; | |
| 190 end | |
| 191 | |
| 192 module:log("debug", "Room %s has %d occupants", bare_jid, count); | |
| 193 return count, nil; | |
| 194 end | |
| 195 | |
| 196 function serve_user(response, format, user_jid) | |
| 197 local presence = get_user_presence(user_jid); | |
| 198 local nickname = get_user_nickname(user_jid) or user_jid; | |
| 199 | |
| 200 local status = presence.status or "offline"; | |
| 201 local message = presence.message or ""; | |
| 202 | |
| 203 if not format or format == "" or format == "full" then | |
| 204 response.headers["Content-Type"] = "text/html"; | |
| 205 return response:send( | |
| 206 [[<!DOCTYPE html>]].. | |
| 207 tostring( | |
| 208 stanza("html") | |
| 209 :tag("head") | |
| 210 :tag("title"):text(nickname):up() | |
| 211 :tag("link", { rel = "stylesheet", href = "data:text/css;base64,"..base64_encode(request_resource("style.css")) }) | |
| 212 :up() | |
| 213 :tag("body") | |
| 214 :tag("table", { width = "100%" }) | |
| 215 :tag("colgroup") | |
| 216 :tag("col", { width = "64px" }):up() | |
| 217 :tag("col"):up() | |
| 218 :up() | |
| 219 :tag("tr") | |
| 220 :tag("td", { rowspan = "3", valign = "top" }) | |
| 221 :tag("img", { id = "avatar", src = "./avatar", width = "64" }) | |
| 222 :up() | |
| 223 :tag("td") | |
| 224 :tag("img", { id = "status-icon", src = "./status-icon", title = status, alt = "("..status..")" }):up() | |
| 225 :tag("b", { id = "nickname"}):text(" "..nickname):up() | |
| 226 :up() | |
| 227 :up() | |
| 228 :tag("tr") | |
| 229 :tag("td", { id = "msg-cell" }):text(message):up() | |
| 230 :up() | |
| 231 :tag("tr") | |
| 232 :tag("td", { id = "jid-cell" }) | |
| 233 :tag("i") | |
| 234 :tag("a", { href = "xmpp:"..user_jid.."?add" }):text(user_jid):up() | |
| 235 :up() | |
| 236 :up() | |
| 237 :up() | |
| 238 ) | |
| 239 ); | |
| 240 elseif format == "nickname" then | |
| 241 response.headers["Content-Type"] = "text/plain"; | |
| 242 return response:send(nickname); | |
| 243 elseif format == "status" then | |
| 244 response.headers["Content-Type"] = "text/plain"; | |
| 245 return response:send(status); | |
| 246 elseif format == "message" then | |
| 247 response.headers["Content-Type"] = "text/plain"; | |
| 248 return response:send(message); | |
| 249 elseif format == "status-icon" then | |
| 250 response.headers["Content-Type"] = "image/png"; | |
| 251 local status_resource = request_resource(status..".png"); | |
| 252 if not status_resource then | |
| 253 return response:send(request_resource("offline.png")); | |
| 254 end | |
| 255 return response:send(status_resource); | |
| 256 elseif format == "avatar" then | |
| 257 local avatar_mime, avatar_data = get_user_avatar(user_jid); | |
| 258 if not avatar_mime or not avatar_data then | |
| 259 response.headers["Content-Type"] = "image/png"; | |
| 260 return response:send(request_resource("avatar.png")); | |
| 261 end | |
| 262 response.headers["Content-Type"] = avatar_mime; | |
| 263 return response:send(base64_decode(avatar_data)); | |
| 264 else | |
| 265 response.headers["Content-Type"] = "text/plain"; | |
| 266 return response:send(status..": "..message); | |
| 267 end | |
| 268 end | |
| 269 | |
| 270 function serve_muc(response, format, muc_jid) | |
| 271 local muc_name, muc_desc, err = get_muc_info(muc_jid); | |
| 272 local muc_users, _ = get_muc_users(muc_jid); | |
| 273 | |
| 274 if not format or format == "" or format == "full" then | |
| 275 response.headers["Content-Type"] = "text/html"; | |
| 276 return response:send( | |
| 277 [[<!DOCTYPE html>]].. | |
| 278 tostring( | |
| 279 stanza("html") | |
| 280 :tag("head") | |
| 281 :tag("title"):text(muc_name or muc_jid):up() | |
| 282 :tag("link", { rel = "stylesheet", href = "data:text/css;base64,"..base64_encode(request_resource("style.css")) }) | |
| 283 :up() | |
| 284 :tag("body") | |
| 285 :tag("table", { width = "100%" }) | |
| 286 :tag("colgroup") | |
| 287 :tag("col", { width = "64px" }):up() | |
| 288 :tag("col"):up() | |
| 289 :up() | |
| 290 :tag("tr") | |
| 291 :tag("td", { rowspan = "3", valign = "top" }) | |
| 292 :tag("img", { id = "avatar", src = "./avatar", width = "64" }) | |
| 293 :up() | |
| 294 :tag("td") | |
| 295 :tag("img", { id = "status-icon", src = "./status-icon", title = "muc", alt = "(muc)" }):up() | |
| 296 :tag("b", { id = "nickname" }):text(" "..(muc_name or muc_jid)):up() | |
| 297 :tag("a", { id = "muc-users" }):text(" ("..muc_users.." users)"):up() | |
| 298 :up() | |
| 299 :up() | |
| 300 :tag("tr") | |
| 301 :tag("td", { id = "msg-cell" }):text(muc_desc):up() | |
| 302 :up() | |
| 303 :tag("tr") | |
| 304 :tag("td", { id = "jid-cell" }) | |
| 305 :tag("i") | |
| 306 :tag("a", { href = "xmpp:"..muc_jid.."?join" }):text(muc_jid):up() | |
| 307 :up() | |
| 308 :up() | |
| 309 :up() | |
| 310 ) | |
| 311 ); | |
| 312 elseif format == "users" then | |
| 313 response.headers["Content-Type"] = "text/plain"; | |
| 314 return response:send(muc_users.." users"); | |
| 315 elseif format == "name" then | |
| 316 response.headers["Content-Type"] = "text/plain"; | |
| 317 return response:send(muc_name); | |
| 318 elseif format == "status" then | |
| 319 response.headers["Content-Type"] = "text/plain"; | |
| 320 return response:send("muc"); | |
| 321 elseif format == "description" then | |
| 322 response.headers["Content-Type"] = "text/plain"; | |
| 323 return response:send(muc_desc); | |
| 324 elseif format == "status-icon" then | |
| 325 response.headers["Content-Type"] = "image/png"; | |
| 326 return response:send(request_resource("muc.png")); | |
| 327 elseif format == "avatar" then | |
| 328 local avatar_mime, avatar_data = get_muc_avatar(muc_jid); | |
| 329 if not avatar_mime or not avatar_data then | |
| 330 response.headers["Content-Type"] = "image/png"; | |
| 331 return response:send(request_resource("avatar.png")); | |
| 332 end | |
| 333 response.headers["Content-Type"] = avatar_mime; | |
| 334 return response:send(base64_decode(avatar_data)); | |
| 335 else | |
| 336 response.headers["Content-Type"] = "text/plain"; | |
| 337 return response:send((muc_name or muc_jid)..": "..(muc_desc or "")); | |
| 338 end | |
| 339 end | |
| 340 | |
| 341 function request_resource(name) | |
| 342 local resource_path = module:get_option_string("presence_resource_path", "resources"); | |
| 343 local i, err = module:load_resource(resource_path.."/"..name); | |
| 344 if not i then | |
| 345 module:log("warn", "Failed to open resource file %s: %s", resource_path.."/"..name, err); | |
| 346 return ""; | |
| 347 end | |
| 348 return i:read("*a"); | |
| 349 end | |
| 350 | |
| 351 function handle_request(event, path) | |
| 352 local request = event.request; | |
| 353 local response = event.response; | |
| 354 local name, format = path:match("^([%w-_\\.]+)/(.*)$"); | |
| 355 module:log("debug", "loading format '%s' for jid %s", format or "standard", name); | |
| 356 | |
| 357 if not name then | |
| 358 response.status_code = 404; | |
| 359 return response:send("Missing JID"); | |
| 360 end | |
| 361 | |
| 362 local bare_jid = jid.join(name, module.host, nil); | |
| 363 local component = hosts[module.host]; | |
| 364 if component.type == "component" and component.modules.muc then | |
| 365 local muc = component.modules.muc; | |
| 366 if not muc.get_room_from_jid(bare_jid) then | |
| 367 response.status_code = 404; | |
| 368 return response:send("MUC does not exist"); | |
| 369 end | |
| 370 return serve_muc(response, format or "full", bare_jid); | |
| 371 else | |
| 372 if not usermanager.user_exists(name, module.host) then | |
| 373 response.status_code = 404; | |
| 374 return response:send("User does not exist"); | |
| 375 end | |
| 376 return serve_user(response, format or "full", bare_jid); | |
| 377 end | |
| 378 end | |
| 379 | |
| 380 module:provides("http", { | |
| 381 default_path = module:get_option_string("presence_http_path", "/presence"); | |
| 382 route = { | |
| 383 ["GET /*"] = handle_request; | |
| 384 }; | |
| 385 }); |