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 });