# HG changeset patch # User Trần H. Trung # Date 1690371825 -25200 # Node ID f7410850941f7573de4558bbb5e1b1d9be85e253 # Parent 14480ca9576e48007e289f918f73cadbe4aa100a mod_invites_api: Change and add new commands for `module.command` to improve UX. diff -r 14480ca9576e -r f7410850941f mod_invites_api/mod_invites_api.lua --- a/mod_invites_api/mod_invites_api.lua Mon Jul 10 19:19:36 2023 +0700 +++ b/mod_invites_api/mod_invites_api.lua Wed Jul 26 18:43:45 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, config, path; -- 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,557 @@ }); 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 config, path = module:get_option("http_paths"); + if config then + for k, v in pairs(config) do + if k == module.name then + path = v; + break; + else + path = "/"..module.name; + end + end + else path = "/"..module.name ; end + + local url_base = module:get_option_string("http_external_link", module.host); + return url_base..path.."?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("Renew:"); print(""); + print("> prosodyctl mod_"..module.name.." renew JID 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 == "renew" then + print("========================================================================"); + print(""); + print("Re-new 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(" 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.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"); 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 found = false; + 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 + found = false; + 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 == "renew" then + local util_token = require "util.id".long(); + local os_time = os.time(); + if username then + local found; + 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 + print("Error: Could not find the key for "..username.."@"..domain); + print(""); + print("To make this API key, run:"); + print("prosodyctl "..module.name.." create "..username.."@"..domain); + return; + 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 + found = false; + 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 + date = datetime.datetime(os_time); + print("Error: Could not find "..arg_name.." in "..domain); + print(""); + print("To make this API key, run:"); + print("prosodyctl "..module.name.." create "..domain.." "..arg_name); + return; + end + return; + end + elseif command == "get" then + local name = table.remove(arg, 1); + local found = false; + 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 + print("Error: could not find "..name.." in "..domain); + print(""); + print("You can create it with:"); + print(""); + print("> prosodyctl "..module.name.." create "..domain.." "..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 + print("Error: could not find the key for "..username.."@"..domain); + print(""); + print("You can create it with:"); + print(""); + print("> prosodyctl "..module.name.." create "..username.."@"..domain); + end + return; + else + get_value(function (id, info) + if info.jid == module.host then + print(date, id, info.jid, info.name or ""); + 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 ""); + 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); + end + end); + return; + elseif username then + get_value(function (id, info) + local j = jid.prepped_split(info.jid); + if j == username then + print(id); + end + end); + return; + else + get_value(function (id, info) + if info.jid == module.host then + print(id, info.jid, info.name or ""); + 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); + end + 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); + end + end); + return; + else + get_value(function (id, info) + if info.jid == module.host then + print(id.."/"..info.token, info.jid, info.name or ""); + 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)); + end + 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)); + end + 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 ""); + 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 + 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 + local arg_id = table.remove(arg, 1); + if not api_key_store:get(nil, arg_id) then print("Error: key not found"); 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 ""); + 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);