--[[ Copyright (c) 2013-2014 Isode Limited. All rights reserved. See the COPYING file for more information. --]] local sluift = select(1, ...) local _G = _G local pairs, ipairs, print, tostring, type, error, assert, next, rawset, xpcall, unpack, io = pairs, ipairs, print, tostring, type, error, assert, next, rawset, xpcall, unpack, io local setmetatable, getmetatable = setmetatable, getmetatable local string = require "string" local table = require "table" local debug = require "debug" _ENV = nil -------------------------------------------------------------------------------- -- Table utility methods -------------------------------------------------------------------------------- local function table_value_tostring(value) local result = tostring(value) if type(value) == 'number' then return result elseif type(value) == 'boolean' then return result elseif type(value) == 'string' then return "'" .. result .. "'" else return '<' .. result .. '>' end end local function table_tostring(table, print_functions, indent, accumulator, history) local INDENT = ' ' local accumulator = accumulator or '' local history = history or {} local indent = indent or '' accumulator = accumulator .. '{' history[table] = true local is_first = true for key, value in pairs(table) do if print_functions or type(value) ~= 'function' then if not is_first then accumulator = accumulator .. ',' end is_first = false accumulator = accumulator .. '\n' .. indent .. INDENT .. '[' .. table_value_tostring(key) .. '] = ' if type(value) == 'table' then if history[value] then accumulator = accumulator .. "..." else accumulator = table_tostring(value, print_functions, indent .. INDENT, accumulator, history) end else accumulator = accumulator .. table_value_tostring(value) end end end history[table] = false if not is_first then accumulator = accumulator .. '\n' .. indent end accumulator = accumulator .. '}' return accumulator end local function register_table_tostring(table, print_functions) if type(table) == 'table' then local metatable = getmetatable(table) if not metatable then metatable = {} setmetatable(table, metatable) end if print_functions then metatable.__tostring = function(table) return table_tostring(table, true) end else metatable.__tostring = table_tostring end end end -- FIXME: Not really a good or efficiant equals, but does the trick for now local function table_equals(t1, t2) return tostring(t1) == tostring(t2) end local function register_table_equals(table) if type(table) == 'table' then local metatable = getmetatable(table) if not metatable then metatable = {} setmetatable(table, metatable) end metatable.__eq = table_equals end end local function merge_tables(...) local result = {} for _, table in ipairs({...}) do for k, v in pairs(table) do result[k] = v end end return result end local function copy(object) if type(object) == 'table' then local copy = {} for key, value in pairs(object) do copy[key] = value end return copy else return object end end local function clear(table) setmetatable(table, nil) for key, value in pairs(table) do rawset(table, key, nil) end end local function trim(string) return string:gsub("^%s*(.-)%s*$", "%1") end local function keys(table) local result = {} for key in pairs(table) do result[#result+1] = key end return result end local function insert_all(table, values) for _, value in pairs(values) do table[#table+1] = value end end -------------------------------------------------------------------------------- -- Help -------------------------------------------------------------------------------- -- Contains help for native methods that we want access to from here local extra_help = {} local component_extra_help = {} local help_data = {} local help_classes = {} local help_class_metatables = {} local _H local function get_synopsis(description) return description:gsub("[\n\r].*", "") end local function format_description(text) local result = {} local trim_whitespace for line in (text .. "\n"):gmatch"(.-)\n" do if not trim_whitespace and line:find('[^%s]') then trim_whitespace = line:match("^(%s*)") end if trim_whitespace then line = line:gsub("^" .. trim_whitespace, "") end table.insert(result, line) end return trim(table.concat(result, "\n")) end local function strip_links(text) return text:gsub("(@{(%w*)})", "`%2`") end local function register_help(target, help) assert(target) if not help then help = _H end assert(help) -- Transform description into canonical representation local parameters = {} for _, parameter in pairs(help.parameters or {}) do local parameter_description = parameter[2] if parameter_description and #parameter_description == 0 then parameter_description = nil end if type(parameter) == "table" then parameters[#parameters+1] = { name = parameter[1], description = parameter_description } else parameters[#parameters+1] = { name = parameter } end end local options = {} for option_name, option_description in pairs(help.options or {}) do if type(option_description) == "table" then options[#options+1] = { name = option_description.name, description = option_description.description } else options[#options+1] = { name = option_name, description = option_description } end end local description = format_description(help[1] or help.description or "") local synopsis = get_synopsis(description) if #description == 0 then synopsis = nil description = nil end local data = { description = description, synopsis = synopsis, parameters = parameters, options = options, classes = help.classes } register_table_tostring(data, true) help_data[target] = data end local function register_class_help(class, help) help_classes[#help_classes+1] = class register_help(class, help) end local function register_class_table_help(target, class, help) register_help(target, help) help_class_metatables[class] = target register_class_help(class, help) end _H = { [[ Retrieves the help information from `target`. Returns a table with the following fields: - `description`: the description of `target` - `parameters`: an array of parameters of `target` represented as tables with `name` and `description` fields. - `options`: an array of options (named parameters) of `target` represented as tables with `name` and `description` fields. - `methods`: an array of methods - `fields`: an array of fields ]], parameters = { {"target", "The target to retrieve help of"} } } local function get_help(target) if not target then error("Nil argument or argument missing") end local help = help_data[target] or help_data[getmetatable(target)] or {} -- Collect child methods and fields local children = {} if type(target) == "table" then children = target end local mt if type(target) == "string" then mt = help_class_metatables[target] else mt = getmetatable(target) end if mt and type(mt.__index) == "table" then children = merge_tables(children, mt.__index) end local methods = {} local fields = {} for name, value in pairs(children) do if name:sub(1, 1) ~= "_" then if type(value) == "function" then methods[#methods+1] = { name = name, ref = value } else fields[#fields+1] = { name = name, description = nil } end end end if next(methods) ~= nil then help.methods = methods end if next(fields) ~= nil then help.fields = fields end if next(help) then return help else return nil end end register_help(get_help) _H = { [[ Prints the help of `target`. `target` can be any object. When `target` is a string, prints the help of the class with the given name. ]], parameters = { {"target", "The target to retrieve help of"} } } local function help(target) print() if not target then print("Call `help(target)` to get the help of a specific `target`.") print("`target` can be any object. When `target` is a string, prints") print("the help of the class with the given name.") print() print("For general information about sluift, type:") print(" help(sluift)") print() return end local data = get_help(target) if not data then print("No help available\n") return end -- Collect help of children local methods = {} for _, method in pairs(data.methods or {}) do local description local method_help = get_help(method.ref) if method_help and method_help.description then description = method_help.synopsis end methods[#methods+1] = { name = method.name, description = description } end local fields = copy(data.fields or {}) table.sort(methods, function (a, b) return (a.name or "") < (b.name or "") end) table.sort(fields, function (a, b) return (a.name or "") < (b.name or "") end) local classes = {} for _, class in pairs(data.classes or {}) do classes[#classes+1] = { name = class, description = get_help(class).synopsis } end print(strip_links(data.description) or "(No description available)") for _, p in ipairs({ {"Parameters", data.parameters}, {"Options", data.options}, {"Methods", methods}, {"Fields", fields}, {"Classes", classes}}) do if p[2] and next(p[2]) ~= nil then print() print(p[1] .. ":") for _, parameter in ipairs(p[2]) do if parameter.description then print(" " .. parameter.name .. ": " .. strip_links(parameter.description)) else print(" " .. parameter.name) end end end end print() end register_help(help) -------------------------------------------------------------------------------- -- Utility methods -------------------------------------------------------------------------------- _H = { [[ Perform a shallow copy of `object`. ]], parameters = {{"object", "the object to copy"}} } register_help(copy) _H = { [[ Pretty-print a table ]], parameters = {{"table", "the table to print"}} } local function tprint(table) print(table_tostring(table, true)) end register_help(tprint) local function remove_help_parameters(elements, table) if type(elements) ~= "table" then elements = {elements} end local result = copy(table) for k, v in ipairs(table) do for _, element in ipairs(elements) do if v.name == element then result[k] = nil end end end return result end local function parse_options(unnamed_parameters, arg1, arg2) local options = {} local f if type(arg1) == 'table' then options = arg1 f = arg2 elseif type(arg1) == 'function' then f = arg1 end options.f = f or options.f return copy(options) end local function get_by_type(table, typ) for _, v in ipairs(table) do if v['_type'] == typ then return v end end end local function register_get_by_type_index(table) if type(table) == 'table' then local metatable = getmetatable(table) if not metatable then metatable = {} setmetatable(table, metatable) end metatable.__index = get_by_type end return table end local function call(options) local f = options[1] local result = { xpcall(f, debug.traceback) } if options.finally then options.finally() end if result[1] then table.remove(result, 1) return unpack(result) else error(result[2]) end end local function read_file(file) local f = io.open(file, 'rb') local result = f:read('*all') f:close() return result end _H = { [[ Generate a form table, suitable for PubSubConfiguration and MAMQuery ]], parameters = { {"fields", "The fields that will be converted into a form table"}, {"form_type", "If specified, add a form_type field with this value"}, {"type", "Form type, e.g. 'submit'"} } } local function create_form(...) local options = parse_options({}, ...) local result = { fields = {} } -- FIXME: make nicer when parse_options binds positional arguments to names if options.fields then for var, value in pairs(options.fields) do result.fields[#result.fields+1] = { name = var, value = value } end elseif options[1] then for var, value in pairs(options[1]) do result.fields[#result.fields+1] = { name = var, value = value } end end if options.form_type then result.fields[#result.fields+1] = { name = 'FORM_TYPE', value = options.form_type } end result['type'] = options.type return result end -------------------------------------------------------------------------------- -- Metatables -------------------------------------------------------------------------------- _H = { [[ Client interface ]] } local Client = { _with_prompt = function(client) return client:jid() end } Client.__index = Client register_class_table_help(Client, "Client") _H = { [[ Component interface ]] } local Component = { _with_prompt = function(component) return component:jid() end } Component.__index = Component register_class_table_help(Component, "Component") _H = { [[ Interface to communicate with a PubSub service ]] } local PubSub = {} PubSub.__index = PubSub register_class_table_help(PubSub, "PubSub") _H = { [[ Interface to communicate with a PubSub node on a service ]] } local PubSubNode = {} PubSubNode.__index = PubSubNode register_class_table_help(PubSubNode, "PubSubNode") -------------------------------------------------------------------------------- -- with -------------------------------------------------------------------------------- local original_G local function with (target, f) -- Dynamic scope if f then with(target) return call{f, finally = function() with() end} end -- No scope if target then if not original_G then original_G = copy(_G) setmetatable(original_G, getmetatable(_G)) clear(_G) end setmetatable(_G, { __index = function(_, key) local value = target[key] if value then if type(value) == 'function' then -- Add 'self' argument to all functions return function(...) return value(target, ...) end else return value end else return original_G[key] end end, __newindex = original_G, _completions = function () local result = {} if type(target) == "table" then insert_all(result, keys(target)) end local mt = getmetatable(target) if mt and type(mt.__index) == 'table' then insert_all(result, keys(mt.__index)) end insert_all(result, keys(original_G)) return result end }) -- Set prompt local prompt = nil -- Try '_with_prompt' in metatable local target_metatable = getmetatable(target) if target_metatable then if type(target_metatable._with_prompt) == "function" then prompt = target_metatable._with_prompt(target) else prompt = target_metatable._with_prompt end end if not prompt then -- Use tostring() local target_string = tostring(target) if string.len(target_string) > 25 then prompt = string.sub(target_string, 0, 22) .. "..." else prompt = target_string end end rawset(_G, "_PROMPT", prompt .. '> ') else -- Reset _G clear(_G) for key, value in pairs(original_G) do _G[key] = value end setmetatable(_G, original_G) end end -------------------------------------------------------------------------------- -- Client -------------------------------------------------------------------------------- extra_help = { ["Client.get_next_event"] = { [[ Returns the next event. ]], parameters = { "self" }, options = { type = "The type of event to return (`message`, `presence`, `pubsub`). When omitted, all event types are returned.", timeout = "The amount of time to wait for events.", ["if"] = "A function to filter events. When this function, called with the event as a parameter, returns true, the event will be returned" } }, ["Client.get"] = { [[ Sends a `get` query. ]], parameters = { "self" }, options = { to = "The JID of the target to send the query to", query = "The query to send", timeout = "The amount of time to wait for the query to finish", } }, ["Client.set"] = { [[ Sends a `set` query. ]], parameters = { "self" }, options = { to = "The JID of the target to send the query to", query = "The query to send.", timeout = "The amount of time to wait for the query to finish.", } }, ["Client.async_connect"] = { [[ Connect to the server asynchronously. This method immediately returns. ]], parameters = { "self" }, options = { host = "The host to connect to. When omitted, is determined by resolving the client JID.", port = "The port to connect to. When omitted, is determined by resolving the client JID." } } } _H = { [[ Connect to the server. This method blocks until the connection has been established. ]], parameters = { "self" }, options = extra_help["Client.async_connect"].options } function Client:connect (...) local options = parse_options({}, ...) local f = options.f self:async_connect(options) self:wait_connected() if f then return call {function() return f(self) end, finally = function() self:disconnect() end} end return true end register_help(Client.connect) _H = { [[ Returns an iterator over all events. This function blocks until `timeout` is reached (or blocks forever if it is omitted). ]], parameters = { "self" }, options = extra_help["Client.get_next_event"].options } function Client:events (options) local function client_events_iterator(s) return s['client']:get_next_event(s['options']) end return client_events_iterator, {client = self, options = options} end register_help(Client.events) _H = { [[ Calls `f` for each event. ]], parameters = { "self" }, options = merge_tables(get_help(Client.events).options, { f = "The functor to call with each event. Required." }) } function Client:for_each_event (...) local options = parse_options({}, ...) if not type(options.f) == 'function' then error('Expected function') end for event in self:events(options) do local result = options.f(event) if result then return result end end end register_help(Client.for_each_event) for method, event_type in pairs({message = 'message', presence = 'presence', pubsub_event = 'pubsub'}) do _H = { "Call `f` for all events of type `" .. event_type .. "`.", parameters = { "self" }, options = remove_help_parameters("type", get_help(Client.for_each_event).options) } Client['for_each_' .. method] = function (client, ...) local options = parse_options({}, ...) options['type'] = event_type return client:for_each_event (options) end register_help(Client['for_each_' .. method]) _H = { "Get the next event of type `" .. event_type .. "`.", parameters = { "self" }, options = remove_help_parameters("type", extra_help["Client.get_next_event"].options) } Client['get_next_' .. method] = function (client, ...) local options = parse_options({}, ...) options['type'] = event_type return client:get_next_event(options) end register_help(Client['get_next_' .. method]) end for method, event_type in pairs({messages = 'message', pubsub_events = 'pubsub'}) do _H = { "Returns an iterator over all events of type `" .. event_type .. "`.", parameters = { "self" }, options = remove_help_parameters("type", get_help(Client.for_each_event).options) } Client[method] = function (client, ...) local options = parse_options({}, ...) options['type'] = event_type return client:events (options) end register_help(Client[method]) end _H = { [[ Process all pending events ]], parameters = { "self" } } function Client:process_events () for event in self:events{timeout=0} do end end register_help(Client.process_events) -- -- Register get_* and set_* convenience methods for some type of queries -- -- Example usages: -- client:get_software_version{to = 'alice@wonderland.lit'} -- client:set_command{to = 'alice@wonderland.lit', command = { type = 'execute', node = 'uptime' }} -- local get_set_shortcuts = { get = {'software_version', 'disco_items', 'xml', 'dom', 'vcard', 'mam'}, set = {'command', 'vcard', 'mam'} } for query_action, query_types in pairs(get_set_shortcuts) do for _, query_type in ipairs(query_types) do _H = { "Sends a `" .. query_action .. "` query of type `" .. query_type .. "`.\n" .. "Apart from the options below, all top level elements of `" .. query_type .. "` can be passed.", parameters = { "self" }, options = remove_help_parameters({"query", "type"}, extra_help["Client.get"].options), } local method = query_action .. '_' .. query_type Client[method] = function (client, options) options = options or {} if type(options) ~= 'table' then error('Invalid options: ' .. options) end options['query'] = merge_tables({_type = query_type}, options[query_type] or {}) return client[query_action](client, options) end register_help(Client[method]) end end _H = { [[ Returns a @{PubSub} object for communicating with the PubSub service at `jid`. ]], parameters = { "self", {"jid", "The JID of the PubSub service"} } } function Client:pubsub (jid) local result = { client = self, jid = jid } setmetatable(result, PubSub) return result end register_help(Client.pubsub) -------------------------------------------------------------------------------- -- Component -------------------------------------------------------------------------------- component_extra_help = { ["Component.get_next_event"] = { [[ Returns the next event. ]], parameters = { "self" }, options = { type = "The type of event to return (`message`, `presence`). When omitted, all event types are returned.", timeout = "The amount of time to wait for events.", ["if"] = "A function to filter events. When this function, called with the event as a parameter, returns true, the event will be returned" } }, ["Component.get"] = { [[ Sends a `get` query. ]], parameters = { "self" }, options = { to = "The JID of the target to send the query to", query = "The query to send", timeout = "The amount of time to wait for the query to finish", } }, ["Component.set"] = { [[ Sends a `set` query. ]], parameters = { "self" }, options = { to = "The JID of the target to send the query to", query = "The query to send.", timeout = "The amount of time to wait for the query to finish.", } }, ["Component.async_connect"] = { [[ Connect to the server asynchronously. This method immediately returns. ]], parameters = { "self" }, options = { host = "The host to connect to.", port = "The port to connect to." } } } _H = { [[ Connect to the server. This method blocks until the connection has been established. ]], parameters = { "self" }, options = component_extra_help["Component.async_connect"].options } function Component:connect (...) local options = parse_options({}, ...) local f = options.f self:async_connect(options) self:wait_connected() if f then return call {function() return f(self) end, finally = function() self:disconnect() end} end return true end register_help(Component.connect) _H = { [[ Returns an iterator over all events. This function blocks until `timeout` is reached (or blocks forever if it is omitted). ]], parameters = { "self" }, options = component_extra_help["Component.get_next_event"].options } function Component:events (options) local function component_events_iterator(s) return s['component']:get_next_event(s['options']) end return component_events_iterator, {component = self, options = options} end register_help(Component.events) _H = { [[ Calls `f` for each event. ]], parameters = { "self" }, options = merge_tables(get_help(Component.events).options, { f = "The functor to call with each event. Required." }) } function Component:for_each_event (...) local options = parse_options({}, ...) if not type(options.f) == 'function' then error('Expected function') end for event in self:events(options) do local result = options.f(event) if result then return result end end end register_help(Component.for_each_event) for method, event_type in pairs({message = 'message', presence = 'presence'}) do _H = { "Call `f` for all events of type `" .. event_type .. "`.", parameters = { "self" }, options = remove_help_parameters("type", get_help(Component.for_each_event).options) } Component['for_each_' .. method] = function (component, ...) local options = parse_options({}, ...) options['type'] = event_type return component:for_each_event (options) end register_help(Component['for_each_' .. method]) _H = { "Get the next event of type `" .. event_type .. "`.", parameters = { "self" }, options = remove_help_parameters("type", component_extra_help["Component.get_next_event"].options) } Component['get_next_' .. method] = function (component, ...) local options = parse_options({}, ...) options['type'] = event_type return component:get_next_event(options) end register_help(Component['get_next_' .. method]) end for method, event_type in pairs({messages = 'message'}) do _H = { "Returns an iterator over all events of type `" .. event_type .. "`.", parameters = { "self" }, options = remove_help_parameters("type", get_help(Component.for_each_event).options) } Component[method] = function (component, ...) local options = parse_options({}, ...) options['type'] = event_type return component:events (options) end register_help(Component[method]) end _H = { [[ Process all pending events ]], parameters = { "self" } } function Component:process_events () for event in self:events{timeout=0} do end end register_help(Component.process_events) -- -- Register get_* and set_* convenience methods for some type of queries -- -- Example usages: -- component:get_software_version{to = 'alice@wonderland.lit'} -- component:set_command{to = 'alice@wonderland.lit', command = { type = 'execute', node = 'uptime' }} -- local get_set_shortcuts = { get = {'software_version', 'disco_items', 'xml', 'dom', 'vcard'}, set = {'command', 'vcard'} } for query_action, query_types in pairs(get_set_shortcuts) do for _, query_type in ipairs(query_types) do _H = { "Sends a `" .. query_action .. "` query of type `" .. query_type .. "`.\n" .. "Apart from the options below, all top level elements of `" .. query_type .. "` can be passed.", parameters = { "self" }, options = remove_help_parameters({"query", "type"}, component_extra_help["Component.get"].options), } local method = query_action .. '_' .. query_type Component[method] = function (component, options) options = options or {} if type(options) ~= 'table' then error('Invalid options: ' .. options) end options['query'] = merge_tables({_type = query_type}, options[query_type] or {}) return component[query_action](component, options) end register_help(Component[method]) end end -------------------------------------------------------------------------------- -- PubSub -------------------------------------------------------------------------------- local function process_pubsub_event (event) if event._type == 'pubsub_event_items' then -- Add 'item' shortcut to payload of first item event.item = event.items and event.items[1] and event.items[1].data and event.items[1].data[1] end end function PubSub:list_nodes (options) return self.client:get_disco_items(merge_tables({to = self.jid}, options)) end function PubSub:node (node) local result = { client = self.client, jid = self.jid, node = node } setmetatable(result, PubSubNode) return result end local simple_pubsub_queries = { get_default_configuration = 'pubsub_owner_default', get_subscriptions = 'pubsub_subscriptions', get_affiliations = 'pubsub_affiliations', get_default_subscription_options = 'pubsub_default', } for method, query_type in pairs(simple_pubsub_queries) do PubSub[method] = function (service, options) options = options or {} return service.client:query_pubsub(merge_tables( { type = 'get', to = service.jid, query = { _type = query_type } }, options)) end end for _, method in ipairs({'events', 'get_next_event', 'for_each_event'}) do PubSub[method] = function (node, ...) local options = parse_options({}, ...) options['if'] = function (event) return event.type == 'pubsub' and event.from == node.jid and event.node == node end return node.client[method](node.client, options) end end -------------------------------------------------------------------------------- -- PubSubNode -------------------------------------------------------------------------------- local function pubsub_node_configuration_to_form(configuration) return create_form{configuration, form_type="http://jabber.org/protocol/pubsub#node_config", type="submit"} end function PubSubNode:list_items (options) return self.client:get_disco_items(merge_tables({to = self.jid, disco_items = { node = self.node }}, options)) end local simple_pubsub_node_queries = { get_configuration = 'pubsub_owner_configure', get_subscriptions = 'pubsub_subscriptions', get_affiliations = 'pubsub_affiliations', get_owner_subscriptions = 'pubsub_owner_subscriptions', get_owner_affiliations = 'pubsub_owner_affiliations', get_default_subscription_options = 'pubsub_default', } for method, query_type in pairs(simple_pubsub_node_queries) do PubSubNode[method] = function (node, options) return node.client:query_pubsub(merge_tables({ type = 'get', to = node.jid, query = { _type = query_type, node = node.node }}, options)) end end function PubSubNode:get_items (...) local options = parse_options({}, ...) local items = options.items or {} if options.maximum_items then items = merge_tables({maximum_items = options.maximum_items}, items) end items = merge_tables({_type = 'pubsub_items', node = self.node}, items) return self.client:query_pubsub(merge_tables({ type = 'get', to = self.jid, query = items}, options)) end function PubSubNode:get_item (...) local options = parse_options({}, ...) if not type(options.id) == 'string' then error('Expected ID') end return self:get_items{items = {{id = options.id}}} end function PubSubNode:create (options) options = options or {} local configure if options['configuration'] then configure = { data = pubsub_node_configuration_to_form(options['configuration']) } end return self.client:query_pubsub(merge_tables( { type = 'set', to = self.jid, query = { _type = 'pubsub_create', node = self.node, configure = configure } }, options)) end function PubSubNode:delete (options) options = options or {} local redirect if options['redirect'] then redirect = {uri = options['redirect']} end return self.client:query_pubsub(merge_tables({ type = 'set', to = self.jid, query = { _type = 'pubsub_owner_delete', node = self.node, redirect = redirect }}, options)) end function PubSubNode:set_configuration(options) options = options or {} local configuration = pubsub_node_configuration_to_form(options['configuration']) return self.client:query_pubsub(merge_tables( { type = 'set', to = self.jid, query = { _type = 'pubsub_owner_configure', node = self.node, data = configuration } }, options)) end function PubSubNode:set_owner_affiliations(...) local options = parse_options({}, ...) return self.client:query_pubsub(merge_tables({ type = 'set', to = self.jid, query = merge_tables({ _type = 'pubsub_owner_affiliations', node = self.node, }, options.affiliations)}, options)) end function PubSubNode:subscribe(...) local options = parse_options({}, ...) local jid = options.jid or sluift.jid.to_bare(self.client:jid()) return self.client:query_pubsub(merge_tables( { type = 'set', to = self.jid, query = { _type = 'pubsub_subscribe', node = self.node, jid = jid } }, options)) end function PubSubNode:unsubscribe(options) options = options or {} return self.client:query_pubsub(merge_tables( { type = 'set', to = self.jid, query = { _type = 'pubsub_unsubscribe', node = self.node, jid = options['jid'], subscription_id = 'subscription_id'} }, options)) end function PubSubNode:get_subscription_options (options) return self.client:query_pubsub(merge_tables( { type = 'get', to = self.jid, query = { _type = 'pubsub_options', node = self.node, jid = options['jid'] } }, options)) end function PubSubNode:publish(...) local options = parse_options({}, ...) local items = options.items or {} if options.item then if type(options.item) == 'string' or options.item._type then items = {{id = options.id, data = { options.item } }} options.id = nil else items = { options.item } end options.item = nil end return self.client:query_pubsub(merge_tables( { type = 'set', to = self.jid, query = { _type = 'pubsub_publish', node = self.node, items = items } }, options)) end function PubSubNode:retract(...) local options = parse_options({}, ...) local items = options.items if options.id then items = {{id = options.id}} end return self.client:query_pubsub(merge_tables( { type = 'set', to = self.jid, query = { _type = 'pubsub_retract', node = self.node, items = items, notify = options['notify'] }}, options)) end function PubSubNode:purge(...) local options = parse_options({}, ...) return self.client:query_pubsub(merge_tables( { type = 'set', to = self.jid, query = { _type = 'pubsub_owner_purge', node = self.node }}, options)) end -- Iterators over events for _, method in ipairs({'events', 'get_next_event', 'for_each_event'}) do PubSubNode[method] = function (node, ...) local options = parse_options({}, ...) options['if'] = function (event) return event.type == 'pubsub' and event.from == node.jid and event.node == node.node end return node.client[method](node.client, options) end end -------------------------------------------------------------------------------- -- Service discovery -------------------------------------------------------------------------------- local disco = { features = { DISCO_INFO = 'http://jabber.org/protocol/disco#info', COMMANDS = 'http://jabber.org/protocol/commands', USER_LOCATION = 'http://jabber.org/protocol/geoloc', USER_TUNE = 'http://jabber.org/protocol/tune', USER_AVATAR_METADATA = 'urn:xmpp:avatar:metadata', USER_ACTIVITY = 'http://jabber.org/protocol/activity', USER_PROFILE = 'urn:xmpp:tmp:profile' } } -------------------------------------------------------------------------------- _H = nil extra_help['sluift'] = { [[ This module provides methods for XMPP communication. The main entry point of this module is the `new_client` method, which creates a new client for communicating with an XMPP server. ]], classes = help_classes } return { Client = Client, Component = Component, register_help = register_help, register_class_help = register_class_help, register_table_tostring = register_table_tostring, register_table_equals = register_table_equals, register_get_by_type_index = register_get_by_type_index, process_pubsub_event = process_pubsub_event, tprint = tprint, read_file = read_file, disco = disco, get_help = get_help, help = help, extra_help = extra_help, component_extra_help = component_extra_help, copy = copy, with = with, create_form = create_form }