diff options
Diffstat (limited to 'Sluift/core.lua')
-rw-r--r-- | Sluift/core.lua | 1243 |
1 files changed, 1243 insertions, 0 deletions
diff --git a/Sluift/core.lua b/Sluift/core.lua new file mode 100644 index 0000000..ffbb5f9 --- /dev/null +++ b/Sluift/core.lua @@ -0,0 +1,1243 @@ +--[[ + Copyright (c) 2013-2014 Remko Tronçon + Licensed under the GNU General Public License. + 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', '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'} +} +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 +} |