summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'Sluift/core.lua')
-rw-r--r--Sluift/core.lua1243
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
+}