Changeset

5663:f6e8165a2ec2 draft

Merge upstream
author Trần H. Trung <xmpp:trần.h.trung@trung.fun>
date Tue, 29 Aug 2023 22:46:27 +0700
parents 5640:5e7e609fae0c (diff) 5659:9ce8ee530438 (current diff)
children 5664:52db2da66680
files
diffstat 6 files changed, 1002 insertions(+), 165 deletions(-) [+]
line wrap: on
line diff
--- a/mod_invites_api/README.markdown	Sun Aug 27 15:33:14 2023 +0200
+++ b/mod_invites_api/README.markdown	Tue Aug 29 22:46:27 2023 +0700
@@ -88,23 +88,23 @@
 Authorization: Bearer HTwALnKL/73UUylA-2ZJbu9x1XMATuIbjWpip8ow1
 ```
 
+You can also create an api key for a specific user:
+
+```
+prosodyctl mod_invites_api create user@example.com
+```
+
 Key management
 ==============
 
-At any time you can view authorized keys using:
-
-```
-prosodyctl mod_invites_api list example.com
-```
-
-This will list out the id of each key, and the name if set:
+To list all the available commands:
 
 ```
-HTwALnKL	My test key
+prosodyctl mod_invites_api help
 ```
 
-You can revoke a key by passing this key id to the 'delete` sub-command:
+To get help on a specific command for example `create`:
 
 ```
-prosodyctl mod_invites_api delete example.com HTwALnKL
-```
\ No newline at end of file
+prosodyctl mod_invites_api help create
+```
--- a/mod_invites_api/mod_invites_api.lua	Sun Aug 27 15:33:14 2023 +0200
+++ b/mod_invites_api/mod_invites_api.lua	Tue Aug 29 22:46:27 2023 +0700
@@ -1,12 +1,22 @@
 local http_formdecode = require "net.http".formdecode;
+local jid = require "util.jid";
+local usermanager = require "core.usermanager";
+local datetime = require "util.datetime";
 
-local api_key_store;
-local invites;
+-- Whether local users can invite other users to create an account on this server
+local allow_user_invites = module:get_option_boolean("allow_user_invites", false);
+-- Who can see and use the contact invite command. It is strongly recommened to
+-- keep this available to all local users. To allow/disallow invite-registration
+-- on the server, use the option above instead.
+local allow_contact_invites = module:get_option_boolean("allow_contact_invites", true);
+
+local api_key_store, api_key_store_kv, invites, date;
 -- COMPAT: workaround to avoid executing inside prosodyctl
 if prosody.shutdown then
 	module:depends("http");
 	api_key_store = module:open_store("invite_api_keys", "map");
 	invites = module:depends("invites");
+	api_key_store_kv = module:open_store("invite_api_keys");
 end
 
 local function get_api_user(request, params)
@@ -58,7 +68,18 @@
 		return 405;
 	end
 
-	local invite = invites.create_account(nil, { source = "api/token/"..api_user.id });
+	local invite;
+	local username, domain = jid.prepped_split(api_user.jid);
+	if username and usermanager.user_exists(username, domain) then
+		local ttl = module:get_option_number("invite_expiry", 86400 * 7);
+		invite = invites.create_contact(username,
+			allow_user_invites, -- allow_registration
+			{ source = "api/token/"..api_user.id }, -- additional_data
+			ttl
+			);
+	else
+		invite = invites.create_account(nil, { source = "api/token/"..api_user.id });
+	end
 	if not invite then
 		return 500;
 	end
@@ -79,49 +100,646 @@
 	});
 end
 
+function get_value(callback)
+	for key_id, key_info in pairs(api_key_store_kv:get(nil) or {}) do
+		date = datetime.datetime(key_info.created_at);
+		callback(key_id, key_info);
+	end
+end
+
+local function get_url(id, token)
+	local url_base = module:context(module.host):http_url(module.name, "/"..module.name);
+	return url_base.."?key="..id.."/"..token.."&redirect=true";
+end
+
 function module.command(arg)
 	if #arg < 2 then
-		print("Usage:");
+		print("========================================================================");
+		print("");
+		print("Create:");
+		print("");
+		print("> prosodyctl mod_"..module.name.." create JID NAME");
+		print("");
+		print("Query:");
+		print("");
+		print("> prosodyctl mod_"..module.name.." get JID NAME");
+		print("> prosodyctl mod_"..module.name.." date JID NAME");
+		print("> prosodyctl mod_"..module.name.." id JID NAME");
+		print("> prosodyctl mod_"..module.name.." key JID NAME");
+		print("> prosodyctl mod_"..module.name.." url JID NAME");
 		print("");
-		print(" prosodyctl mod_"..module.name.." create NAME");
-		print(" prosodyctl mod_"..module.name.." delete KEY_ID");
-		print(" prosodyctl mod_"..module.name.." list");
+		print("Revoke:");
+		print("");
+		print("> prosodyctl mod_"..module.name.." delete JID NAME");
+		print("> prosodyctl mod_"..module.name.." delete-id HOST ID");
+		print("");
+		print("Adjust:");
 		print("");
+		print("> prosodyctl mod_"..module.name.." renew JID NAME");
+		print("> prosodyctl mod_"..module.name.." rename JID NAME NEW_NAME");
+		print("");
+		print("Help:");
+		print("");
+		print("> prosodyctl mod_"..module.name.." help COMMAND");
+		print("");
+		print("========================================================================");
+		return;
 	end
 
 	local command = table.remove(arg, 1);
+	if command == "help" then
+		help_command = table.remove(arg, 1);
+		if help_command == "create" then
+			print("========================================================================");
+			print("");
+			print("Create a key:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." create JID NAME");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" `JID` can either be a user's account or a host.");
+			print(" When `JID` is a host, you need to supply a `NAME`.");
+			print("");
+			print(" Each user's account can only have 1 API key but hosts are unlimited.");
+			print("");
+			print("========================================================================");
+		elseif help_command == "rename" then
+			print("========================================================================");
+			print("");
+			print("Re-name a key:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." rename JID NAME NEW_NAME");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" `JID` can only be a valid host.");
+			print("");
+			print(" You need to supply a `NEW_NAME` to replace « `NAME` » the old one.");
+			print("");
+			print("========================================================================");
+		elseif help_command == "renew" then
+			print("========================================================================");
+			print("");
+			print("Re-new a key:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." renew JID NAME");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" `JID` can either be a user's account or a host.");
+			print(" When `JID` is a host, you need to supply a `NAME`.");
+			print("");
+			print(" The old `ID` will be kept and a new token will be generated for the API");
+			print(" key you specified.");
+			print("");
+			print("========================================================================");
+		elseif help_command == "get" then
+			print("========================================================================");
+			print("");
+			print("Get info of a key:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." get JID NAME");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" When `JID` is a domain, it will list all the keys under that host.");
+			print(" When `JID` is a user's account, it fetches the key for that user.");
+			print(" If you supply both a host and a `NAME`, it fetches the key with `NAME`");
+			print(" under that host.")
+			print("");
+			print(" Output for a host is: DATE, ID, JID, NAME.");
+			print("");
+			print(" Output for a user's account is: DATE, ID, URL.");
+			print("");
+			print(" Output for a host with a valid `NAME` is: DATE, ID, URL.");
+			print("");
+			print("========================================================================");
+		elseif help_command == "date" then
+			print("========================================================================");
+			print("");
+			print("Get the time stamp of a key:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." date JID NAME");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" Same as the `get` command but print only the birthday of the key.");
+			print("");
+			print("========================================================================");
+		elseif help_command == "id" then
+			print("========================================================================");
+			print("");
+			print("Print the ID of a key:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." id JID NAME");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" Same as the `get` command but print only the ID of the key.");
+			print("");
+			print("========================================================================");
+		elseif help_command == "key" then
+			print("========================================================================");
+			print("");
+			print("Print the API key:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." key JID NAME");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" Same as the `get` command but print only the key.");
+			print("");
+			print(" The key has the format: ID/TOKEN");
+			print("");
+			print(" The `renew` command will generate a new token and revoke the old one.");
+			print("");
+			print("========================================================================");
+		elseif help_command == "url" then
+			print("========================================================================");
+			print("");
+			print("Print the URL of a key:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." url JID NAME");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" Same as the `get` command but print only the URL of the key.");
+			print("");
+			print("========================================================================");
+		elseif help_command == "delete" then
+			print("========================================================================");
+			print("");
+			print("Delete a key by JID and NAME:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." delete JID NAME");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" Same as `create` command but delete the key specified.");
+			print("");
+			print("========================================================================");
+		elseif help_command == "delete-id" then
+			print("========================================================================");
+			print("");
+			print("Delete API key by ID:");
+			print("");
+			print("> prosodyctl mod_"..module.name.." delete HOST ID");
+			print("");
+			print("------------------------------------------------------------------------");
+			print("");
+			print("Usage:");
+			print("");
+			print(" Input must be a valid host - cannot be a user's account.");
+			print("");
+			print(" To get the ID of a key, use the `id` command.");
+			print("");
+			print("========================================================================");
+		end
+		return;
+	end
 
-	local host = table.remove(arg, 1);
-	if not prosody.hosts[host] then
-		print("Error: please supply a valid host");
+	local username, domain = jid.prepped_split(table.remove(arg, 1));
+	if not prosody.hosts[domain] then
+		print("Error: please supply a valid host.");
 		return 1;
 	end
-	require "core.storagemanager".initialize_host(host);
-	module.host = host; --luacheck: ignore 122/module
+	require "core.storagemanager".initialize_host(domain);
+	module:depends("http");
+	module.host = domain; --luacheck: ignore 122/module
+	usermanager.initialize_host(module.host);
 	api_key_store = module:open_store("invite_api_keys", "map");
+	api_key_store_kv = module:open_store("invite_api_keys");
+
+	local found = false;
+	local function error_found(username, name)
+		if name then
+			print("Error: Could not find "..name.." in "..domain);
+			print("");
+			print("To make this API key, run:");
+			print("> prosodyctl "..module.name.." create "..domain.." "..name);
+			return 1;
+		end
+		if username then
+			print("Error: Could not find "..username.."@"..domain);
+			print("");
+			print("To make this API key, run:");
+			print("> prosodyctl "..module.name.." create "..username.."@"..domain);
+			return 1;
+		end
+	end
 
 	if command == "create" then
-		local id = require "util.id".short();
-		local token = require "util.id".long();
-		api_key_store:set(nil, id, {
-			id = id;
-			token = token;
-			name = arg[1];
-			created_at = os.time();
-			allowed_methods = { GET = true, POST = true };
-		});
-		print(id.."/"..token);
+		local util_id = require "util.id".short();
+		local util_token = require "util.id".long();
+		local os_time = os.time();
+		if username then
+			if usermanager.user_exists(username, module.host) then
+				get_value(function (id, info)
+					if username.."@"..module.host == info.jid then
+						date = datetime.datetime(info.created_at);
+						print("Found:");
+						print(date, id, info.jid);
+						util_id = id;
+						util_token = info.token;
+						found = true;
+					end
+				end);
+				if found == false then
+					api_key_store:set(nil, util_id, {
+						id = util_id;
+						token = util_token;
+						name = nil;
+						jid = username.."@"..domain;
+						created_at = os_time;
+						allowed_methods = { GET = true, POST = true };
+					});
+					date = datetime.datetime(os_time);
+					print("Created:");
+					print(date, util_id.."/"..util_token, username.."@"..domain);
+				end
+				return;
+			else
+				print("Error: "..username.."@"..domain.." does not exists.");
+				return 1;
+			end
+		elseif domain then
+			local arg_name = table.remove(arg, 1);
+			if not arg_name or arg_name == "" then
+				print("Error: key for host needs a `NAME`.");
+				return;
+			end
+			get_value(function (id, info)
+				if domain == info.jid and arg_name == info.name then
+					date = datetime.datetime(info.created_at);
+					print("Found:");
+					print(date, id, info.jid, info.name);
+					util_id = id;
+					util_token = info.token;
+					found = true;
+				end
+			end);
+			if found == false then
+				api_key_store:set(nil, util_id, {
+					id = util_id;
+					token = util_token;
+					name = arg_name;
+					jid = domain;
+					created_at = os_time;
+					allowed_methods = { GET = true, POST = true };
+				});
+				date = datetime.datetime(os_time);
+				print("Created:");
+				print(date, util_id.."/"..util_token, domain, arg_name);
+			end
+			return;
+		end
+	elseif command == "rename" then
+		local util_token = require "util.id".long();
+		local os_time = os.time();
+		if username then
+			if usermanager.user_exists(username, module.host) then
+				get_value(function (id, info)
+					if username.."@"..module.host == info.jid then
+						print("Found:");
+						print(date, id.."/"..util_token, info.jid);
+						found = true;
+					end
+				end);
+				if not found then
+					error_found(username);
+					return 1;
+				end
+				print("Error: not allow to rename user's account.");
+				return 1;
+			else
+				print("Error: "..username.."@"..domain.." doesn't exists!");
+				return 1;
+			end
+		elseif domain then
+			local arg_name_orig = table.remove(arg, 1);
+			if not arg_name_orig or arg_name_orig == "" then
+				print("Error: key for host needs a `NAME`.");
+				return 1;
+			end
+			local arg_name_new = table.remove(arg, 1);
+			if not arg_name_new or arg_name_new == "" then
+				print("Error: need a `NEW_NAME` to replace "..arg_name_orig);
+				return 1;
+			end
+			get_value(function (id, info)
+				if domain == info.jid and arg_name_orig == info.name then
+					api_key_store:set(nil, id, {
+						id = id;
+						token = info.token;
+						name = arg_name_new;
+						jid = domain;
+						created_at = os_time;
+						allowed_methods = { GET = true, POST = true };
+					});
+					date = datetime.datetime(os_time);
+					print("Re-named:");
+					print(date, id.."/"..info.token, info.jid, arg_name_new);
+					found = true;
+				end
+			end);
+			if not found then
+				error_found(nil, arg_name_orig);
+				return 1;
+			end
+			return;
+		end
+	elseif command == "renew" then
+		local util_token = require "util.id".long();
+		local os_time = os.time();
+		if username then
+			if usermanager.user_exists(username, module.host) then
+				get_value(function (id, info)
+					if username.."@"..module.host == info.jid then
+						api_key_store:set(nil, id, {
+							id = id;
+							token = util_token;
+							name = arg[1];
+							jid = username.."@"..domain;
+							created_at = os_time;
+							allowed_methods = { GET = true, POST = true };
+						});
+						found = true;
+						date = datetime.datetime(os_time);
+						print("Re-newed:");
+						print(date, id.."/"..util_token, info.jid);
+					end
+				end);
+				if not found then
+					error_found(username);
+					return 1;
+				end
+				return;
+			else
+				print("Error: "..username.."@"..domain.." does not exists.");
+				return 1;
+			end
+		elseif domain then
+			local arg_name = table.remove(arg, 1);
+			if not arg_name or arg_name == "" then
+				print("Error: key for host needs a `NAME`.");
+				return 1;
+			end
+			get_value(function (id, info)
+				if domain == info.jid and arg_name == info.name then
+					api_key_store:set(nil, id, {
+						id = id;
+						token = util_token;
+						name = arg_name;
+						jid = domain;
+						created_at = os_time;
+						allowed_methods = { GET = true, POST = true };
+					});
+					date = datetime.datetime(os_time);
+					print("Re-newed:");
+					print(date, id.."/"..util_token, info.jid, info.name);
+					found = true;
+				end
+			end);
+			if not found then
+				error_found(nil, arg_name);
+				return 1;
+			end
+			return;
+		end
+	elseif command == "get" then
+		local name = table.remove(arg, 1);
+		if name and not username then
+			get_value(function (id, info)
+				if info.name == name then
+					print(date, id, get_url(id, info.token));
+					found = true;
+				end
+			end);
+			if found == false then
+				error_found(nil, name);
+			end
+			return;
+		elseif username then
+			get_value(function (id, info)
+				local j = jid.prepped_split(info.jid);
+				if j == username then
+					print(date, id, get_url(id, info.token));
+					found = true;
+				end
+			end);
+			if found == false then
+				error_found(username);
+			end
+			return;
+		else
+			get_value(function (id, info)
+				if info.jid == module.host then
+					print(date, id, info.jid, info.name or "<unknown>");
+				else
+					print(date, id, info.jid, info.name or "");
+				end
+			end);
+			return;
+		end
+	elseif command == "date" then
+		local name = table.remove(arg, 1);
+		if name and not username then
+			get_value(function (id, info)
+				if info.name == name then
+					print(date);
+				end
+			end);
+			return;
+		elseif username then
+			get_value(function (id, info)
+				local j = jid.prepped_split(info.jid);
+				if j == username then
+					print(date);
+				end
+			end);
+			return;
+		else
+			get_value(function (id, info)
+				if info.jid == module.host then
+					print(date, info.jid, info.name or "<unknown>");
+				else
+					print(date, info.jid);
+				end
+			end);
+			return;
+		end
+	elseif command == "id" then
+		local name = table.remove(arg, 1);
+		if name and not username then
+			get_value(function (id, info)
+				if info.name == name then
+					print(id);
+					found = true;
+				end
+			end);
+			if not found then
+				error_found(nil, name);
+				return 1;
+			end
+			return;
+		elseif username then
+			get_value(function (id, info)
+				local j = jid.prepped_split(info.jid);
+				if j == username then
+					print(id);
+					found = true
+				end
+			end);
+			if not found then
+				error_found(username);
+				return 1;
+			end
+			return;
+		else
+			get_value(function (id, info)
+				if info.jid == module.host then
+					print(id, info.jid, info.name or "<unknown>");
+				else
+					print(id, info.jid);
+				end
+			end);
+			return;
+		end
+	elseif command == "key" then
+		local name = table.remove(arg, 1);
+		if name and not username then
+			get_value(function (id, info)
+				if info.name == name then
+					print(id.."/"..info.token);
+					found = true;
+				end
+			end);
+			if not found then
+				error_found(nil, name);
+				return 1;
+			end
+			return;
+		elseif username then
+			get_value(function (id, info)
+				local j = jid.prepped_split(info.jid);
+				if j == username then
+					print(id.."/"..info.token);
+					found = true;
+				end
+			end);
+			if not found then
+				error_found(username);
+				return 1;
+			end
+			return;
+		else
+			get_value(function (id, info)
+				if info.jid == module.host then
+					print(id.."/"..info.token, info.jid, info.name or "<unknown>");
+				else
+					print(id.."/"..info.token, info.jid);
+				end
+			end);
+			return;
+		end
+	elseif command == "url" then
+		local name = table.remove(arg, 1);
+		if name and not username then
+			get_value(function (id, info)
+				if info.name == name then
+					print(get_url(id, info.token));
+					found = true;
+				end
+			end);
+			if not found then
+				error_found(nil, name);
+				return 1;
+			end
+			return;
+		elseif username then
+			get_value(function (id, info)
+				local j = jid.prepped_split(info.jid);
+				if j == username then
+					print(get_url(id, info.token));
+					found = true;
+				end
+			end);
+			if not found then
+				error_found(username);
+				return 1;
+			end
+			return;
+		else
+			get_value(function (id, info)
+				if info.jid == module.host then
+					print(get_url(id, info.token), info.jid, info.name or "<unknown>");
+				else
+					print(get_url(id, info.token), info.jid);
+				end
+			end);
+			return;
+		end
 	elseif command == "delete" then
-		local id = table.remove(arg, 1);
-		if not api_key_store:get(nil, id) then
-			print("Error: key not found");
+		local name = table.remove(arg, 1);
+		if name and not username then
+			get_value(function (id, info)
+				if info.name == name then
+					api_key_store:set(nil, id, nil);
+					print("Deleted:");
+					print(date, id.."/"..info.token, info.jid, info.name or "");
+				end
+			end);
+			return;
+		elseif username then
+			get_value(function (id, info)
+				local j = jid.prepped_split(info.jid);
+				if j == username then
+					api_key_store:set(nil, id, nil);
+					print("Deleted:");
+					print(date, id.."/"..info.token, info.jid, info.name or "");
+				end
+			end);
+			return;
+		else
+			print("Error: Needs a valid `JID`. Or a host and a `NAME`.");
+		end
+	elseif command == "delete-id" then
+		if username then
+			print("Error: Input must be a valid host - cannot be a user's account.");
 			return 1;
 		end
-		api_key_store:set(nil, id, nil);
-	elseif command == "list" then
-		local api_key_store_kv = module:open_store("invite_api_keys");
-		for key_id, key_info in pairs(api_key_store_kv:get(nil) or {}) do
-			print(key_id, key_info.name or "<unknown>");
+		local arg_id = table.remove(arg, 1);
+		if not api_key_store:get(nil, arg_id) then
+			print("Error: key not found!");
+			return 1;
+		else
+			get_value(function (id, info)
+				if arg_id == id then
+					api_key_store:set(nil, id, nil);
+					print("Deleted:");
+					print(date, id, info.jid, info.name or "");
+				end
+			end);
+			return;
 		end
 	else
 		print("Unknown command - "..command);
--- a/mod_invites_register_web/README.markdown	Sun Aug 27 15:33:14 2023 +0200
+++ b/mod_invites_register_web/README.markdown	Tue Aug 29 22:46:27 2023 +0700
@@ -34,13 +34,6 @@
 validates invite tokens. It also supports guiding the user through client
 download and configuration via mod_register_apps.
 
-There is no specific configuration for this module (though it uses the
-optional `site_name` to override the displayed site name.
-
-You may also set `webchat_url` to the URL of a web chat that will be linked
-to after successful registration. If not specified but mod_conversejs is loaded
-on the current host, it will default to the URL of that module.
-
 This module depends on mod_invites_page solely for the case where an invalid
 invite token is received - it will redirect to mod_invites_page so that an
 appropriate error can be served to the user.
@@ -49,3 +42,41 @@
 loaded). As a consequence of this module being loaded, the default password
 policies will be enforced for all registrations on the server if not
 explicitly loaded or configured.
+
+Configuration
+=============
+
+It uses the optional `site_name` to override the displayed site name.
+
+You can set `webchat_url` to the URL of a web chat that will be linked
+to after successful registration. If not specified but mod_conversejs is loaded
+on the current host, it will default to the URL of that module.
+
+You can use your own html templates with `invites_html_template`. Names of the
+files MUST match the default. More over, you can offer multiple (human)
+languages by adding the `&l=` to the URL. Meaning this module will serve
+`register.html` for your default URL:
+```
+
+    https://prosody.example.net/?=aowiefjoaij
+
+``` 
+
+And if you have a `register.en.html` in the directory you have specified in
+your config file, it will be served at:
+```
+
+    https://prosody.example.net/?=aowiefjoaij&l=en
+
+```
+
+So in your `register.html`, you can point to the English version by using an
+`<a>` tag like this:
+```
+
+    <a href="/?={token}&l=en">English</a>
+
+```
+
+You can further customize your URL with [mod_invites_page] too.
+
--- a/mod_invites_register_web/mod_invites_register_web.lua	Sun Aug 27 15:33:14 2023 +0200
+++ b/mod_invites_register_web/mod_invites_register_web.lua	Tue Aug 29 22:46:27 2023 +0700
@@ -34,10 +34,17 @@
 local invites = module:depends("invites");
 local invites_page = module:depends("invites_page");
 
+local templatePath = module:get_option_string("invites_html_template", "html");
+function template_get(sTemplate, sLang)
+	local template = sLang and templatePath.."/"..sTemplate.."."..sLang..".html" 
+		or templatePath.."/"..sTemplate..".html";
+	return assert(module:load_resource(template):read("*a"));
+end
+
+local lang = nil;
 function serve_register_page(event)
-	local register_page_template = assert(module:load_resource("html/register.html")):read("*a");
-
 	local query_params = event.request.url.query and http_formdecode(event.request.url.query);
+	lang = query_params and query_params.l;
 
 	local invite = query_params and invites.get(query_params.t);
 	if not invite then
@@ -49,6 +56,7 @@
 		};
 	end
 
+	local register_page_template = template_get("register", lang);
 	event.response.headers["Content-Type"] = "text/html; charset=utf-8";
 
 	local invite_page = render_html_template(register_page_template, {
@@ -71,8 +79,8 @@
 	local user, password, token = form_data["user"], form_data["password"], form_data["token"];
 	local app_id = form_data["app_id"];
 
-	local register_page_template = assert(module:load_resource("html/register.html")):read("*a");
-	local error_template = assert(module:load_resource("html/register_error.html")):read("*a");
+	local register_page_template = template_get("register", lang);
+	local error_template = template_get("register_error", lang);
 
 	local invite = invites.get(token);
 	if not invite then
@@ -94,6 +102,7 @@
 			uri = invite.uri;
 			type = invite.type;
 			jid = invite.jid;
+			inviter = invite.inviter;
 			password_policy = mod_password_policy.get_policy();
 
 			msg_class = "alert-warning";
@@ -112,6 +121,7 @@
 			uri = invite.uri;
 			type = invite.type;
 			jid = invite.jid;
+			inviter = invite.inviter;
 			password_policy = mod_password_policy.get_policy();
 
 			msg_class = "alert-warning";
@@ -127,6 +137,7 @@
 			uri = invite.uri;
 			type = invite.type;
 			jid = invite.jid;
+			inviter = invite.inviter;
 			password_policy = mod_password_policy.get_policy();
 
 			msg_class = "alert-warning";
@@ -145,6 +156,7 @@
 			uri = invite.uri;
 			type = invite.type;
 			jid = invite.jid;
+			inviter = invite.inviter;
 			password_policy = mod_password_policy.get_policy();
 
 			msg_class = "alert-warning";
@@ -163,6 +175,8 @@
 	module:fire_event("user-registering", registering);
 
 	if not registering.allowed then
+		prepped_username = nil;
+		password = nil;
 		return render_html_template(error_template, {
 			site_name = site_name;
 			msg_class = "alert-danger";
@@ -201,9 +215,9 @@
 				};
 			end
 			-- If recognised app, we serve a page that includes setup instructions
-			success_template = assert(module:load_resource("html/register_success_setup.html")):read("*a");
+			success_template = template_get("register_success_setup", lang);
 		else
-			success_template = assert(module:load_resource("html/register_success.html")):read("*a");
+			success_template = template_get("register_success", lang);
 		end
 
 		-- Due to the credentials being served here, ensure that
--- a/mod_webpresence/README.markdown	Sun Aug 27 15:33:14 2023 +0200
+++ b/mod_webpresence/README.markdown	Tue Aug 29 22:46:27 2023 +0700
@@ -2,65 +2,80 @@
 labels:
 - 'Stage-Stable'
 summary: Display your online status in web pages
+rockspec:
+  build:
+    copy_directories:
+    - icons
 ...
 
 Introduction
 ============
 
 Quite often you may want to publish your Jabber status to your blog or
-website. mod\_webpresence allows you to do exactly this.
-
-Details
-=======
-
-This module uses Prosody's built-in HTTP server (it does not depend on
-mod\_httpserver). It supplies a status icon representative of a user's
-online state.
+website. mod\_webpresence allows you to do exactly this via adhoc control.
 
 Installation
 ============
 
-Simply copy mod\_webpresence.lua to your modules directory, the image
-files are embedded within it. Then add "webpresence" to your
-modules\_enabled list.
+Copy mod\_webpresence.lua to your modules directory then add it to your
+modules\_enabled list:
+
+```
 
-Usage
-=====
+    modules_enabled = {
+        "webpresence";
+    };
+
+```
+
+Configuration & Usage
+=====================
+
+There is a set of icons supplied with the module. But you can configure it to
+load your own in the config file:
 
-Once loaded you can embed the icon into a page using a simple `<img>`
-tag, as follows:
+```
+
+    webpresence_icons = "/path/to/your/icons";    
 
-    <img src="http://prosody.example.com:5280/status/john.smith" />
+```
+
+Beware that the icon files must have the same names as the default files.
+
+This module will always returns offline until you enable it via adhoc.
 
-Alternatively, it can be used to get status name as plaint text, status
-message as plain text or html-code for embedding on web-pages.
+You can embed the icon into a page using a simple `<img>` tag, as follows:
+
+    <img src="http://prosody.example.com:5280/status/john.smith@domain.net" />
 
-To get status name in plain text you can use something like that link:
-`http://prosody.example.com:5280/status/john.smith/text`
+Alternatively, it can be used to get status name as plain text, status message
+as plain text or html-code for embedding on web-pages.
+
+To get status name in plain text you can use something like this link:
+`http://prosody.example.com:5280/status/john.smith@domain.net/text`
 
 To get status message as plain text you can use something like following
-link: `http://prosody.example.com:5280/status/john.smith/message`
+link: `http://prosody.example.com:5280/status/john.smith@domain.net/message`
 
-To get html code, containig status name, status image and status message
-(if set): `http://prosody.example.com:5280/status/john.smith/html`
-
-All other
+To get html code, containing status name, status image and status message
+(if set): `http://prosody.example.com:5280/status/john.smith@domain.net/html`
 
 Compatibility
 =============
 
-  ----- -------
-  trunk   Works
-  0.10   Works
-  0.9   Works
-  0.8   Works
-  0.7   Works
-  0.6   Works
-  ----- -------
+  -----     -------
+  trunk     Works
+  0.12.3    Works
+  0.10      Works
+  0.9       Works
+  0.8       Works
+  0.7       Works
+  0.6       Works
+  -----     -------
 
 Todo
 ====
 
 -   Display PEP information (maybe a new plugin?)
--   More (free) iconsets
 -   Internal/external image generator (GD, ImageMagick)
+-   Display the correct boolean in the first form.
--- a/mod_webpresence/mod_webpresence.lua	Sun Aug 27 15:33:14 2023 +0200
+++ b/mod_webpresence/mod_webpresence.lua	Tue Aug 29 22:46:27 2023 +0700
@@ -1,13 +1,161 @@
+module:depends("adhoc");
 module:depends("http");
 
+local moduleHost = module.host;
+local moduleName = module:get_name();
+local jid = require "util.jid";
 local jid_split = require "util.jid".prepped_split;
+local serialization = require "util.serialization";
+
+-- ADHOC
+local storage = module:open_store(moduleName, "keyval");
+local utilDataforms = require "util.dataforms";
+local utilAdhoc = require "util.adhoc";
+local adhoc_new = module:require("adhoc").new;
+
+local function webpresence_set(user, value)
+	local ok, err = storage:set(user, value); -- value is table.
+
+	if not ok or err then 
+		module:log(error, "Could not write data %s", tostring(user));
+		return ok, err;
+	else
+		return ok;
+	end
+end
+local function webpresence_get(user) 
+	local result = storage:get(user);
+	if not result then
+		result = { ["webpresence"] = false };
+		webpresence_set(user, result);
+	end
+	return result[moduleName]; -- bool
+end
+
+local form = utilDataforms.new {
+	title = "Web Presence Policy";
+	instructions = "Your webpresence shows offline by default";
+	{
+		type = "boolean";
+		name = moduleName;
+		label = "Show";
+		--value = webpresence_get();
+	};
+};
+local formResult = utilDataforms.new {
+	title = "Web Presence Policy";
+	{
+		type = "boolean";
+		name = moduleName;
+		label = "Show";
+		--value;
+	};
+	{
+		type = "text-multi";
+		name = "url";
+		label = "Check your presence at";
+		value = "text-multi\n";
+	};
+};
+
+local function webpresence_url(jid)
+	local path = "/status";
+	local config = module:get_option("http_paths");
+	if config then
+		for k, v in pairs(config) do
+			if k == moduleName then 
+				path = v;
+				break;
+			end
+		end
+	end
+
+	local urlBase = module:context(module.host):http_url(module.name, path);
+	local style = { "text", "message", "json", "html" };
+	local urlResult = urlBase.."/"..jid.."\n";
+	for _, v in ipairs(style) do
+		urlResult = urlResult..urlBase.."/"..jid.."/"..v.."\n";
+	end
+	return urlResult;
+end
+
+-- TODO:
+-- Fix the handler (somehow) to make `form` shows the correct value.
+--
+local adhoc_handler = utilAdhoc.new_simple_form(form, function(fields, state, data)
+	local jid_bare = jid.bare(data.from);
+	local user, host = jid_split(jid_bare);
+
+	local oldData, _ = storage:get(user);
+	oldValue = webpresence_get(user);
+	form.webpresence = oldValue;
+
+	local urlResult = webpresence_url(jid_bare);
+	local newValue = {
+		[moduleName] = fields.webpresence;
+	};
+
+	if state then
+		return {
+			status = "completed";
+			info = "No change for: "..tostring(data.from).." …\n"
+				.."Old data: "..serialization.serialize(oldData).."\n"
+				.."New data: "..serialization.serialize(newValue).."\n";
+			result = {
+				layout = formResult;
+				values = {
+					webpresence = oldValue;
+					url = urlResult;
+				};
+			};
+		};
+	else
+		local resultSet, resultErr = webpresence_set(user, newValue)
+		if not resultSet or resultErr then
+			module:log("error", moduleName..": ".."Could not set value for "..user.."@"..host..": %s", errOut);
+			return {
+				status = "completed";
+				info = "Could not set value: "..tostring(data.from).." …\n"
+					.."Old data: "..serialization.serialize(oldData).."\n"
+					.."New data: "..serialization.serialize(newValue).."\n"
+					.."Error: "..errOut.."\n";
+				result = {
+					layout = formResult;
+					values = {
+						webpresence = newValue[moduleName];
+						url = urlResult;
+					};
+				};
+			};
+		else
+			return {
+				status = "completed";
+				info = "Changing value for: "..tostring(data.from).." …\n"
+					.."Old data: "..serialization.serialize(oldData).."\n"
+					.."New data: "..serialization.serialize(newValue).."\n";
+				result = {
+					layout = formResult;
+					values = {
+						webpresence = newValue[moduleName];
+						url = urlResult;
+					};
+				};
+			};
+		end
+	end
+end);
+
+module:provides("adhoc", adhoc_new("Web Presence Policy", moduleName, adhoc_handler, "any"));
+
+-- HTTP
 local b64 = require "util.encodings".base64.encode;
 local sha1 = require "util.hashes".sha1;
 local stanza = require "util.stanza".stanza;
 local json = require "util.json".encode_ordered;
+local usermanager = require "core.usermanager";
 
 local function require_resource(name)
-    local icon_path = module:get_option_string("presence_icons", "icons");
+    local icon_path = module:get_option_string("webpresence_icons", "icons");
     local f, err  = module:load_resource(icon_path.."/"..name);
     if f then
         return f:read("*a");
@@ -18,101 +166,112 @@
 
 local statuses = { online = {}, away = {}, xa = {}, dnd = {}, chat = {}, offline = {} };
 
+local function user_list(host) return user:list(host); end
+
 local function handle_request(event, path)
-  local status, message;
-  local jid, type = path:match("([^/]+)/?(.*)$");
-  if jid then
-    local user, host = jid_split(jid);
-    if host and not user then
-        user, host = host, event.request.headers.host;
-        if host then host = host:gsub(":%d+$", ""); end
-    end
-    if user and host then
-      local user_sessions = hosts[host] and hosts[host].sessions[user];
-      if user_sessions and user_sessions.top_resources then
-        status = user_sessions.top_resources[1];
-        if status and status.presence then
-          message = status.presence:child_with_name("status");
-          status = status.presence:child_with_name("show");
-          if not status then
-            status = "online";
-          else
-            status = status:get_text();
-          end
-          if message then
-            message = message:get_text();
-          end
-        end
-      end
-    end
-  end
-  status = status or "offline";
+	local status, message;
+	local jid, type = path:match("([^/]+)/?(.*)$");
+	if jid then
+		local user, host = jid_split(jid);
+		if host and not user then
+			user, host = host, event.request.headers.host;
+			if host then host = host:gsub(":%d+$", ""); end
+		end
+		if host ~= moduleHost then 
+			status = "offline";
+		else
+			if user and host and usermanager.user_exists(user, host) then
+				local user_sessions = hosts[host] and hosts[host].sessions[user];
+				local show = webpresence_get(user)
+				if show == false then 
+					status = "offline";
+				else
+					if user_sessions and user_sessions.top_resources then
+						status = user_sessions.top_resources[1];
+						if status and status.presence then
+							message = status.presence:child_with_name("status");
+							status = status.presence:child_with_name("show");
+							if not status then
+								status = "online";
+							else
+								status = status:get_text();
+							end
+							if message then
+								message = message:get_text();
+							end
+						end
+					end
+				end
+			end
+		end
+		status = status or "offline";
+	end
 
-  statuses[status].image = function()
-    return { status_code = 200, headers = { content_type = "image/png" },
-      body =  require_resource("status_"..status..".png")
-    };
+statuses[status].image = function()
+	return { status_code = 200, headers = { content_type = "image/png" },
+	body =  require_resource("status_"..status..".png")
+};
   end;
   statuses[status].html = function()
-    local jid_hash = sha1(jid, true);
-    return { status_code = 200, headers = { content_type = "text/html" },
-      body =  [[<!DOCTYPE html>]]..
-        tostring(
-          stanza("html")
-            :tag("head")
-            :tag("title"):text("XMPP Status Page for "..jid):up():up()
-            :tag("body")
-            :tag("div", { id = jid_hash.."_status", class = "xmpp_status" })
-            :tag("img", { id = jid_hash.."_img", class = "xmpp_status_image xmpp_status_"..status,
-              src = "data:image/png;base64,"..b64(require_resource("status_"..status..".png")) }):up()
-            :tag("span", { id = jid_hash.."_status_name", class = "xmpp_status_name" })
-              :text("\194\160"..status):up()
-            :tag("span", { id = jid_hash.."_status_message", class = "xmpp_status_message" })
-              :text(message and "\194\160"..message.."" or "")
-        )
-    };
+	  local jid_hash = sha1(jid, true);
+	  return { status_code = 200, headers = { content_type = "text/html" },
+	  body =  [[<!DOCTYPE html>]]..
+	  tostring(
+	  stanza("html")
+	  :tag("head")
+	  :tag("title"):text("XMPP Status Page for "..jid):up():up()
+	  :tag("body")
+	  :tag("div", { id = jid_hash.."_status", class = "xmpp_status" })
+	  :tag("img", { id = jid_hash.."_img", class = "xmpp_status_image xmpp_status_"..status,
+	  src = "data:image/png;base64,"..b64(require_resource("status_"..status..".png")) }):up()
+	  :tag("span", { id = jid_hash.."_status_name", class = "xmpp_status_name" })
+	  :text("\194\160"..status):up()
+	  :tag("span", { id = jid_hash.."_status_message", class = "xmpp_status_message" })
+	  :text(message and "\194\160"..message.."" or "")
+	  )
+  };
   end;
   statuses[status].text = function()
-    return { status_code = 200, headers = { content_type = "text/plain" },
-      body = status
-    };
+	  return { status_code = 200, headers = { content_type = "text/plain" },
+	  body = status
+  };
   end;
   statuses[status].message = function()
-    return { status_code = 200, headers = { content_type = "text/plain" },
-      body = (message and message or "")
-    };
+	  return { status_code = 200, headers = { content_type = "text/plain" },
+	  body = (message and message or "")
+  };
   end;
   statuses[status].json = function()
-    return { status_code = 200, headers = { content_type = "application/json" },
-      body = json({
-        jid    = jid,
-        show   = status,
-        status = (message and message or "null")
-      })
-    };
+	  return { status_code = 200, headers = { content_type = "application/json" },
+	  body = json({
+		  jid    = jid,
+		  show   = status,
+		  status = (message and message or "null")
+	  })
+  };
   end;
   statuses[status].xml = function()
-    return { status_code = 200, headers = { content_type = "application/xml" },
-      body = [[<?xml version="1.0" encoding="utf-8"?>]]..
-        tostring(
-          stanza("result")
-            :tag("jid"):text(jid):up()
-            :tag("show"):text(status):up()
-            :tag("status"):text(message)
-        )
-      };
+	  return { status_code = 200, headers = { content_type = "application/xml" },
+	  body = [[<?xml version="1.0" encoding="utf-8"?>]]..
+	  tostring(
+	  stanza("result")
+	  :tag("jid"):text(jid):up()
+	  :tag("show"):text(status):up()
+	  :tag("status"):text(message)
+	  )
+  };
   end
 
   if ((type == "") or (not statuses[status][type])) then
-    type = "image"
+	  type = "image"
   end;
 
   return statuses[status][type]();
 end
 
 module:provides("http", {
-    default_path = "/status";
-    route = {
-        ["GET /*"] = handle_request;
-    };
+	default_path = "/status";
+	route = {
+		["GET /*"] = handle_request;
+	};
 });