Software /
code /
prosody-modules
File
mod_http_admin_api/mod_http_admin_api.lua @ 4515:2e33eeafe962
mod_muc_markers: Prevent any markers from reaching the archive, even if untracked
Original intention was to leave alone things that this module isn't
handling. However markers in archives are just problematic without
more advanced logic about what is markable and what is not. It also
requires a more advanced query in mod_muc_rai to determine the latest
markable message instead of the latest archived message.
I'd rather keep the "is archivable" and "is markable" definition the
same for simplicity. I don't want to introduce yet another set of rules
for no reason.
No markers in MAM.
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Mon, 22 Mar 2021 15:55:02 +0000 |
parent | 4396:de55e1475808 |
child | 4516:5bc706c2db8f |
line wrap: on
line source
local usermanager = require "core.usermanager"; local json = require "util.json"; module:depends("http"); local invites = module:depends("invites"); local tokens = module:depends("tokenauth"); local mod_pep = module:depends("pep"); local mod_groups = module:depends("groups_internal"); local push_errors = module:shared("cloud_notify/push_errors"); local site_name = module:get_option_string("site_name", module.host); local json_content_type = "application/json"; local www_authenticate_header = ("Bearer realm=%q"):format(module.host.."/"..module.name); local function check_credentials(request) local auth_type, auth_data = string.match(request.headers.authorization or "", "^(%S+)%s(.+)$"); if not (auth_type and auth_data) then return false; end if auth_type == "Bearer" then local token_info = tokens.get_token_info(auth_data); if not token_info or not token_info.session then return false; end return token_info.session; end return nil; end function check_auth(routes) local function check_request_auth(event) local session = check_credentials(event.request); if not session then event.response.headers.authorization = www_authenticate_header; return false, 401; elseif session.auth_scope ~= "prosody:scope:admin" then return false, 403; end event.session = session; return true; end for route, handler in pairs(routes) do routes[route] = function (event, ...) local permit, code = check_request_auth(event); if not permit then return code; end return handler(event, ...); end; end return routes; end local function token_info_to_invite_info(token_info) local additional_data = token_info.additional_data; local groups = additional_data and additional_data.groups or nil; local source = additional_data and additional_data.source or nil; local reset = not not (additional_data and additional_data.allow_reset or nil); return { id = token_info.token; type = token_info.type; reusable = not not token_info.reusable; inviter = token_info.inviter; jid = token_info.jid; uri = token_info.uri; landing_page = token_info.landing_page; created_at = token_info.created_at; expires = token_info.expires; groups = groups; source = source; reset = reset; }; end function list_invites(event) local invites_list = {}; for token, invite in invites.pending_account_invites() do --luacheck: ignore 213/token table.insert(invites_list, token_info_to_invite_info(invite)); end table.sort(invites_list, function (a, b) return a.created_at < b.created_at; end); event.response.headers["Content-Type"] = json_content_type; return json.encode_array(invites_list); end function get_invite_by_id(event, invite_id) local invite = invites.get_account_invite_info(invite_id); if not invite then return 404; end event.response.headers["Content-Type"] = json_content_type; return json.encode(token_info_to_invite_info(invite)); end function create_invite_type(event, invite_type) local options; local request = event.request; if request.body and #request.body > 0 then if request.headers.content_type ~= json_content_type then module:log("warn", "Invalid content type"); return 400; end options = json.decode(event.request.body); if not options then module:log("warn", "Invalid JSON"); return 400; end else options = {}; end local source = event.session.username .. "@" .. module.host .. "/admin_api"; local invite; if invite_type == "reset" then if not options.username then return 400; end invite = invites.create_account_reset(options.username, options.ttl); elseif invite_type == "group" then if not options.groups then return 400; end invite = invites.create_group(options.groups, { source = source; }, options.ttl); elseif invite_type == "account" then invite = invites.create_account(options.username, { source = source; groups = options.groups; }, options.ttl); else return 400; end if not invite then return 500; end event.response.headers["Content-Type"] = json_content_type; return json.encode(token_info_to_invite_info(invite)); end function delete_invite(event, invite_id) --luacheck: ignore 212/event if not invites.delete_account_invite(invite_id) then return 404; end return 200; end local function get_user_info(username) if not usermanager.user_exists(username, module.host) then return nil; end local display_name; do local pep_service = mod_pep.get_pep_service(username); local ok, _, nick_item = pep_service:get_last_item("http://jabber.org/protocol/nick", true); if ok and nick_item then display_name = nick_item:get_child_text("nick", "http://jabber.org/protocol/nick"); end end return { username = username; display_name = display_name; }; end local function get_session_debug_info(session) local info = { full_jid = session.full_jid; ip = session.ip; since = math.floor(session.conntime); status = { connected = not not session.conn; hibernating = not not session.hibernating; }; features = { carbons = not not session.want_carbons; encrypted = not not session.secure; acks = not not session.smacks; resumption = not not session.resumption_token; mobile_optimization = not not session.csi_counter; push_notifications = not not session.push_identifier; history = not not session.mam_requested; }; queues = {}; }; -- CSI if session.state then info.status.active = session.state == "active"; info.queues.held_stanzas = session.csi_counter or 0; end -- Smacks queue if session.last_requested_h and session.last_acknowledged_stanza then info.queues.awaiting_acks = session.last_requested_h - session.last_acknowledged_stanza; end if session.push_identifier then info.push_info = { id = session.push_identifier; wakeup_push_sent = session.first_hibernated_push; }; end return info; end local function get_user_omemo_info(username) local everything_valid = true; local any_device = false; local omemo_status = {}; local omemo_devices; local pep_service = mod_pep.get_pep_service(username); if pep_service and pep_service.nodes then local ok, _, device_list = pep_service:get_last_item("eu.siacs.conversations.axolotl.devicelist", true); if ok and device_list then device_list = device_list:get_child("list", "eu.siacs.conversations.axolotl"); end if device_list then omemo_devices = {}; for device_entry in device_list:childtags("device") do any_device = true; local device_info = {}; local device_id = tonumber(device_entry.attr.id or ""); if device_id then device_info.id = device_id; local bundle_id = ("eu.siacs.conversations.axolotl.bundles:%d"):format(device_id); local have_bundle, _, bundle = pep_service:get_last_item(bundle_id, true); if have_bundle and bundle and bundle:get_child("bundle", "eu.siacs.conversations.axolotl") then device_info.have_bundle = true; local config_ok, bundle_config = pep_service:get_node_config(bundle_id, true); if config_ok and bundle_config then device_info.bundle_config = bundle_config; if bundle_config.max_items == 1 and bundle_config.access_model == "open" and bundle_config.persist_items == true and bundle_config.publish_model == "publishers" then device_info.valid = true; end end end end if device_info.valid == nil then device_info.valid = false; everything_valid = false; end table.insert(omemo_devices, device_info); end local config_ok, list_config = pep_service:get_node_config("eu.siacs.conversations.axolotl.devicelist", true); if config_ok and list_config then omemo_status.config = list_config; if list_config.max_items == 1 and list_config.access_model == "open" and list_config.persist_items == true and list_config.publish_model == "publishers" then omemo_status.config_valid = true; end end if omemo_status.config_valid == nil then omemo_status.config_valid = false; everything_valid = false; end end end omemo_status.valid = everything_valid and any_device; return { status = omemo_status; devices = omemo_devices; }; end local function get_user_debug_info(username) local debug_info = { time = os.time(); }; -- Online sessions do local user_sessions = hosts[module.host].sessions[username]; if user_sessions then user_sessions = user_sessions.sessions end local sessions = {}; if user_sessions then for _, session in pairs(user_sessions) do table.insert(sessions, get_session_debug_info(session)); end end debug_info.sessions = sessions; end -- Push registrations do local store = module:open_store("cloud_notify"); local services = store:get(username); local push_registrations = {}; if services then for identifier, push_info in pairs(services) do push_registrations[identifier] = { since = push_info.timestamp; service = push_info.jid; node = push_info.node; error_count = push_errors[identifier] or 0; }; end end debug_info.push_registrations = push_registrations; end -- OMEMO debug_info.omemo = get_user_omemo_info(username); return debug_info; end function list_users(event) local user_list = {}; for username in usermanager.users(module.host) do table.insert(user_list, get_user_info(username)); end event.response.headers["Content-Type"] = json_content_type; return json.encode_array(user_list); end function get_user_by_name(event, username) local property do local name, sub_path = username:match("^([^/]+)/(%w+)$"); if name then username = name; property = sub_path; end end if property == "groups" then event.response.headers["Content-Type"] = json_content_type; return json.encode(mod_groups.get_user_groups(username)); elseif property == "debug" then event.response.headers["Content-Type"] = json_content_type; return json.encode(get_user_debug_info(username)); end local user_info = get_user_info(username); if not user_info then return 404; end event.response.headers["Content-Type"] = json_content_type; return json.encode(user_info); end function delete_user(event, username) --luacheck: ignore 212/event if not usermanager.delete_user(username, module.host) then return 404; end return 200; end function list_groups(event) local group_list = {}; for group_id in mod_groups.groups() do local group_info = mod_groups.get_info(group_id); table.insert(group_list, { id = group_id; name = group_info.name; muc_jid = group_info.muc_jid; members = mod_groups.get_members(group_id); }); end event.response.headers["Content-Type"] = json_content_type; return json.encode_array(group_list); end function get_group_by_id(event, group_id) local group = mod_groups.get_info(group_id); if not group then return 404; end event.response.headers["Content-Type"] = json_content_type; return json.encode({ id = group_id; name = group.name; muc_jid = group.muc_jid; members = mod_groups.get_members(group_id); }); end function create_group(event) local request = event.request; if request.headers.content_type ~= json_content_type or (not request.body or #request.body == 0) then return 400; end local group = json.decode(event.request.body); if not group then return 400; end if not group.name then module:log("warn", "Group missing name property"); return 400; end local create_muc = group.create_muc and true or false; local group_id = mod_groups.create( { name = group.name; }, create_muc ); if not group_id then return 500; end event.response.headers["Content-Type"] = json_content_type; local info = mod_groups.get_info(group_id); return json.encode({ id = group_id; name = info.name; muc_jid = info.muc_jid or nil; members = {}; }); end function update_group(event, group) --luacheck: ignore 212/event -- Add member local group_id, member_name = group:match("^([^/]+)/members/([^/]+)$"); if group_id and member_name then if not mod_groups.add_member(group_id, member_name) then return 500; end return 204; end local group_id = group:match("^([^/]+)$") if group_id then local request = event.request; if request.headers.content_type ~= json_content_type or (not request.body or #request.body == 0) then return 400; end local update = json.decode(event.request.body); if not update then return 400; end local group_info = mod_groups.get_info(group_id); if not group_info then return 404; end if update.name then group_info["name"] = update.name; end if mod_groups.set_info(group_id, group_info) then return 204; else return 500; end end return 404; end function delete_group(event, subpath) --luacheck: ignore 212/event -- Check if this is a membership deletion and handle it local group_id, member_name = subpath:match("^([^/]+)/members/([^/]+)$"); if group_id and member_name then if mod_groups.remove_member(group_id, member_name) then return 204; else return 500; end else -- Action refers to the group group_id = subpath; end if not group_id then return 400; end if not mod_groups.exists(group_id) then return 404; end if not mod_groups.delete(group_id) then return 500; end return 204; end local function get_server_info(event) event.response.headers["Content-Type"] = json_content_type; return json.encode({ site_name = site_name; version = prosody.version; }); end module:provides("http", { route = check_auth { ["GET /invites"] = list_invites; ["GET /invites/*"] = get_invite_by_id; ["POST /invites/*"] = create_invite_type; ["DELETE /invites/*"] = delete_invite; ["GET /users"] = list_users; ["GET /users/*"] = get_user_by_name; ["DELETE /users/*"] = delete_user; ["GET /groups"] = list_groups; ["GET /groups/*"] = get_group_by_id; ["POST /groups"] = create_group; ["PUT /groups/*"] = update_group; ["DELETE /groups/*"] = delete_group; ["GET /server/info"] = get_server_info; }; });