Compare commits
157 Commits
1.0-Beta4
...
1.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d9e8df16 | ||
|
|
58aa944a5e | ||
|
|
dd41a85efc | ||
|
|
e2d18170b0 | ||
|
|
e219c0f976 | ||
|
|
4266ee0fa4 | ||
|
|
3a62908703 | ||
|
|
1469323350 | ||
|
|
a81162edd2 | ||
|
|
8080b4cae2 | ||
|
|
1735c194cd | ||
|
|
6997b2fbf8 | ||
|
|
b81ab97900 | ||
|
|
384e1a92b7 | ||
|
|
ec9aed5b75 | ||
|
|
7152d93dd0 | ||
|
|
4bb226147a | ||
|
|
6b274a2543 | ||
|
|
6a07e7cbe3 | ||
|
|
9f1648328f | ||
|
|
8e9b3d0760 | ||
|
|
5399fdd3a9 | ||
|
|
016aac69f6 | ||
|
|
1d891858b6 | ||
|
|
f02b0564e0 | ||
|
|
2e1befc8c6 | ||
|
|
87ccd804ff | ||
|
|
d7ba7cc72a | ||
|
|
d94811d81d | ||
|
|
6d45971411 | ||
|
|
84e2589f22 | ||
|
|
a3d111be12 | ||
|
|
4916a12b6f | ||
|
|
d6f2e51b05 | ||
|
|
feb7f892ea | ||
|
|
a420771665 | ||
|
|
f2449009d1 | ||
|
|
833f95b53a | ||
|
|
4b1fadde4b | ||
|
|
86a6b40fd8 | ||
|
|
7ef6abb2a3 | ||
|
|
dbf6780345 | ||
|
|
450c313340 | ||
|
|
996ca52471 | ||
|
|
6244857746 | ||
|
|
5635265203 | ||
|
|
45ccb31356 | ||
|
|
1a81b2f464 | ||
|
|
77251452c1 | ||
|
|
4df3aa569b | ||
|
|
2e2e16e281 | ||
|
|
d709f8db65 | ||
|
|
75584d7ad7 | ||
|
|
e0f9025e7c | ||
|
|
9004e8bbf2 | ||
|
|
8b5511c7ec | ||
|
|
34f6195ca5 | ||
|
|
70af52d74c | ||
|
|
ca2b4a188a | ||
|
|
0d32638379 | ||
|
|
c4b1212c44 | ||
|
|
3463bf46c6 | ||
|
|
13a01beb07 | ||
|
|
145f577bde | ||
|
|
30da68f47b | ||
|
|
72ead3d598 | ||
|
|
4b71fba64c | ||
|
|
1ed06bebcd | ||
|
|
aa1996eba6 | ||
|
|
683f717cf7 | ||
|
|
8dbe6f6546 | ||
|
|
5313338c3a | ||
|
|
cd800d636a | ||
|
|
40642b2cd1 | ||
|
|
35ef8f9090 | ||
|
|
38dc35840e | ||
|
|
b4004cd4d6 | ||
|
|
0c8a8314b2 | ||
|
|
4e757c2b56 | ||
|
|
c3be6ea0b2 | ||
|
|
da332365d4 | ||
|
|
f7e7bf601e | ||
|
|
6f4c2f22f3 | ||
|
|
493df57035 | ||
|
|
897a9ac333 | ||
|
|
acc2d071ac | ||
|
|
d3b1f8c476 | ||
|
|
f1db2fc156 | ||
|
|
2004ddd678 | ||
|
|
cb85d4a529 | ||
|
|
ead3af3135 | ||
|
|
a2891d7608 | ||
|
|
d7dea0c6cc | ||
|
|
632827f213 | ||
|
|
b71550cec7 | ||
|
|
b68e7bed40 | ||
|
|
4be6482ff3 | ||
|
|
a21178007f | ||
|
|
2e6c27f665 | ||
|
|
0a3a7b5a70 | ||
|
|
3a12cdbd13 | ||
|
|
7d93d1824b | ||
|
|
ba0d699d83 | ||
|
|
c6ac40c476 | ||
|
|
fe3f8dde4b | ||
|
|
acdf9e2d22 | ||
|
|
2076d506b4 | ||
|
|
68ce47c905 | ||
|
|
7c7fa0f008 | ||
|
|
a8e3657487 | ||
|
|
13a2f719f4 | ||
|
|
2908751020 | ||
|
|
8b29431cde | ||
|
|
4b145958fa | ||
|
|
e08b0054b2 | ||
|
|
596e135a03 | ||
|
|
e55e213c78 | ||
|
|
8749f5e09b | ||
|
|
b3353183f3 | ||
|
|
f97f6e5985 | ||
|
|
34c374a1e1 | ||
|
|
506eccf84d | ||
|
|
982bf3b2ec | ||
|
|
53a5026301 | ||
|
|
0aee445e69 | ||
|
|
cbc42c29fb | ||
|
|
874c51d74d | ||
|
|
f9ac95ddb7 | ||
|
|
0ea014fe41 | ||
|
|
62b190d0ff | ||
|
|
4b57b8131f | ||
|
|
988a90a176 | ||
|
|
67775fb8bd | ||
|
|
e81683beee | ||
|
|
d9c25ee65c | ||
|
|
1ebc7f4d4b | ||
|
|
2c5b77ae2e | ||
|
|
d8aae88526 | ||
|
|
2f4bdfee1b | ||
|
|
f4451fe6b7 | ||
|
|
8d4e77aba6 | ||
|
|
f474d378ef | ||
|
|
defc252c7d | ||
|
|
19bd1e0485 | ||
|
|
5f2fc67c40 | ||
|
|
8ead33fc3b | ||
|
|
ab25301953 | ||
|
|
291b118aca | ||
|
|
db7fb10e95 | ||
|
|
60d3afe6b6 | ||
|
|
afeb8f3f7c | ||
|
|
cdbc0570ca | ||
|
|
e648f08bad | ||
|
|
7ba6d5e02d | ||
|
|
ea48bb5ac5 | ||
|
|
6ee8a2980c | ||
|
|
b8114b25ed |
@@ -33,7 +33,7 @@ class testps(sleekxmpp.ClientXMPP):
|
|||||||
self.node = "pstestnode_%s"
|
self.node = "pstestnode_%s"
|
||||||
self.pshost = pshost
|
self.pshost = pshost
|
||||||
if pshost is None:
|
if pshost is None:
|
||||||
self.pshost = self.server
|
self.pshost = self.boundjid.host
|
||||||
self.nodenum = int(nodenum)
|
self.nodenum = int(nodenum)
|
||||||
self.leafnode = self.nodenum + 1
|
self.leafnode = self.nodenum + 1
|
||||||
self.collectnode = self.nodenum + 2
|
self.collectnode = self.nodenum + 2
|
||||||
|
|||||||
199
examples/adhoc_provider.py
Executable file
199
examples/adhoc_provider.py
Executable file
@@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import getpass
|
||||||
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
|
||||||
|
# Python versions before 3.0 do not use UTF-8 encoding
|
||||||
|
# by default. To ensure that Unicode is handled properly
|
||||||
|
# throughout SleekXMPP, we will set the default encoding
|
||||||
|
# ourselves to UTF-8.
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
reload(sys)
|
||||||
|
sys.setdefaultencoding('utf8')
|
||||||
|
|
||||||
|
|
||||||
|
class CommandBot(sleekxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A simple SleekXMPP bot that provides a basic
|
||||||
|
adhoc command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, jid, password):
|
||||||
|
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||||
|
|
||||||
|
# The session_start event will be triggered when
|
||||||
|
# the bot establishes its connection with the server
|
||||||
|
# and the XML streams are ready for use. We want to
|
||||||
|
# listen for this event so that we we can intialize
|
||||||
|
# our roster.
|
||||||
|
self.add_event_handler("session_start", self.start)
|
||||||
|
|
||||||
|
def start(self, event):
|
||||||
|
"""
|
||||||
|
Process the session_start event.
|
||||||
|
|
||||||
|
Typical actions for the session_start event are
|
||||||
|
requesting the roster and broadcasting an intial
|
||||||
|
presence stanza.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
event -- An empty dictionary. The session_start
|
||||||
|
event does not provide any additional
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.send_presence()
|
||||||
|
self.get_roster()
|
||||||
|
|
||||||
|
# We add the command after session_start has fired
|
||||||
|
# to ensure that the correct full JID is used.
|
||||||
|
|
||||||
|
# If using a component, may also pass jid keyword parameter.
|
||||||
|
|
||||||
|
self['xep_0050'].add_command(node='greeting',
|
||||||
|
name='Greeting',
|
||||||
|
handler=self._handle_command)
|
||||||
|
|
||||||
|
def _handle_command(self, iq, session):
|
||||||
|
"""
|
||||||
|
Respond to the intial request for a command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The iq stanza containing the command request.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
form = self['xep_0004'].makeForm('form', 'Greeting')
|
||||||
|
form.addField(var='greeting',
|
||||||
|
ftype='text-single',
|
||||||
|
label='Your greeting')
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = self._handle_command_complete
|
||||||
|
session['has_next'] = False
|
||||||
|
|
||||||
|
# Other useful session values:
|
||||||
|
# session['to'] -- The JID that received the
|
||||||
|
# command request.
|
||||||
|
# session['from'] -- The JID that sent the
|
||||||
|
# command request.
|
||||||
|
# session['has_next'] = True -- There are more steps to complete
|
||||||
|
# session['allow_complete'] = True -- Allow user to finish immediately
|
||||||
|
# and possibly skip steps
|
||||||
|
# session['cancel'] = handler -- Assign a handler for if the user
|
||||||
|
# cancels the command.
|
||||||
|
# session['notes'] = [ -- Add informative notes about the
|
||||||
|
# ('info', 'Info message'), command's results.
|
||||||
|
# ('warning', 'Warning message'),
|
||||||
|
# ('error', 'Error message')]
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _handle_command_complete(self, payload, session):
|
||||||
|
"""
|
||||||
|
Process a command result from the user.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
payload -- Either a single item, such as a form, or a list
|
||||||
|
of items or forms if more than one form was
|
||||||
|
provided to the user. The payload may be any
|
||||||
|
stanza, such as jabber:x:oob for out of band
|
||||||
|
data, or jabber:x:data for typical data forms.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# In this case (as is typical), the payload is a form
|
||||||
|
form = payload
|
||||||
|
|
||||||
|
greeting = form['values']['greeting']
|
||||||
|
self.send_message(mto=session['from'],
|
||||||
|
mbody="%s, World!" % greeting)
|
||||||
|
|
||||||
|
# Having no return statement is the same as unsetting the 'payload'
|
||||||
|
# and 'next' session values and returning the session.
|
||||||
|
|
||||||
|
# Unless it is the final step, always return the session dictionary.
|
||||||
|
|
||||||
|
session['payload'] = None
|
||||||
|
session['next'] = None
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Setup the command line arguments.
|
||||||
|
optp = OptionParser()
|
||||||
|
|
||||||
|
# Output verbosity options.
|
||||||
|
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.ERROR, default=logging.INFO)
|
||||||
|
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.DEBUG, default=logging.INFO)
|
||||||
|
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=5, default=logging.INFO)
|
||||||
|
|
||||||
|
# JID and password options.
|
||||||
|
optp.add_option("-j", "--jid", dest="jid",
|
||||||
|
help="JID to use")
|
||||||
|
optp.add_option("-p", "--password", dest="password",
|
||||||
|
help="password to use")
|
||||||
|
|
||||||
|
opts, args = optp.parse_args()
|
||||||
|
|
||||||
|
# Setup logging.
|
||||||
|
logging.basicConfig(level=opts.loglevel,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
if opts.jid is None:
|
||||||
|
opts.jid = raw_input("Username: ")
|
||||||
|
if opts.password is None:
|
||||||
|
opts.password = getpass.getpass("Password: ")
|
||||||
|
|
||||||
|
# Setup the CommandBot and register plugins. Note that while plugins may
|
||||||
|
# have interdependencies, the order in which you register them does
|
||||||
|
# not matter.
|
||||||
|
xmpp = CommandBot(opts.jid, opts.password)
|
||||||
|
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||||
|
xmpp.register_plugin('xep_0004') # Data Forms
|
||||||
|
xmpp.register_plugin('xep_0050') # Adhoc Commands
|
||||||
|
|
||||||
|
# If you are working with an OpenFire server, you may need
|
||||||
|
# to adjust the SSL version used:
|
||||||
|
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||||
|
|
||||||
|
# If you want to verify the SSL certificates offered by a server:
|
||||||
|
# xmpp.ca_certs = "path/to/ca/cert"
|
||||||
|
|
||||||
|
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||||
|
if xmpp.connect():
|
||||||
|
# If you do not have the pydns library installed, you will need
|
||||||
|
# to manually specify the name of the server if it does not match
|
||||||
|
# the one in the JID. For example, to use Google Talk you would
|
||||||
|
# need to use:
|
||||||
|
#
|
||||||
|
# if xmpp.connect(('talk.google.com', 5222)):
|
||||||
|
# ...
|
||||||
|
xmpp.process(threaded=False)
|
||||||
|
print("Done")
|
||||||
|
else:
|
||||||
|
print("Unable to connect.")
|
||||||
208
examples/adhoc_user.py
Executable file
208
examples/adhoc_user.py
Executable file
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import getpass
|
||||||
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
|
||||||
|
# Python versions before 3.0 do not use UTF-8 encoding
|
||||||
|
# by default. To ensure that Unicode is handled properly
|
||||||
|
# throughout SleekXMPP, we will set the default encoding
|
||||||
|
# ourselves to UTF-8.
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
reload(sys)
|
||||||
|
sys.setdefaultencoding('utf8')
|
||||||
|
|
||||||
|
|
||||||
|
class CommandUserBot(sleekxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A simple SleekXMPP bot that uses the adhoc command
|
||||||
|
provided by the adhoc_provider.py example.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, jid, password, other, greeting):
|
||||||
|
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||||
|
|
||||||
|
self.command_provider = other
|
||||||
|
self.greeting = greeting
|
||||||
|
|
||||||
|
# The session_start event will be triggered when
|
||||||
|
# the bot establishes its connection with the server
|
||||||
|
# and the XML streams are ready for use. We want to
|
||||||
|
# listen for this event so that we we can intialize
|
||||||
|
# our roster.
|
||||||
|
self.add_event_handler("session_start", self.start)
|
||||||
|
self.add_event_handler("message", self.message)
|
||||||
|
|
||||||
|
def start(self, event):
|
||||||
|
"""
|
||||||
|
Process the session_start event.
|
||||||
|
|
||||||
|
Typical actions for the session_start event are
|
||||||
|
requesting the roster and broadcasting an intial
|
||||||
|
presence stanza.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
event -- An empty dictionary. The session_start
|
||||||
|
event does not provide any additional
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.send_presence()
|
||||||
|
self.get_roster()
|
||||||
|
|
||||||
|
# We first create a session dictionary containing:
|
||||||
|
# 'next' -- the handler to execute on a successful response
|
||||||
|
# 'error' -- the handler to execute if an error occurs
|
||||||
|
|
||||||
|
# The session may also contain custom data.
|
||||||
|
|
||||||
|
session = {'greeting': self.greeting,
|
||||||
|
'next': self._command_start,
|
||||||
|
'error': self._command_error}
|
||||||
|
|
||||||
|
self['xep_0050'].start_command(jid=self.command_provider,
|
||||||
|
node='greeting',
|
||||||
|
session=session)
|
||||||
|
|
||||||
|
def message(self, msg):
|
||||||
|
"""
|
||||||
|
Process incoming message stanzas.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg -- The received message stanza.
|
||||||
|
"""
|
||||||
|
logging.info(msg['body'])
|
||||||
|
|
||||||
|
def _command_start(self, iq, session):
|
||||||
|
"""
|
||||||
|
Process the initial command result.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The iq stanza containing the command result.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The greeting command provides a form with a single field:
|
||||||
|
# <x xmlns="jabber:x:data" type="form">
|
||||||
|
# <field var="greeting"
|
||||||
|
# type="text-single"
|
||||||
|
# label="Your greeting" />
|
||||||
|
# </x>
|
||||||
|
|
||||||
|
form = self['xep_0004'].makeForm(ftype='submit')
|
||||||
|
form.addField(var='greeting',
|
||||||
|
value=session['greeting'])
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
|
||||||
|
# We don't need to process the next result.
|
||||||
|
session['next'] = None
|
||||||
|
|
||||||
|
# Other options include using:
|
||||||
|
# continue_command() -- Continue to the next step in the workflow
|
||||||
|
# cancel_command() -- Stop command execution.
|
||||||
|
|
||||||
|
self['xep_0050'].complete_command(session)
|
||||||
|
|
||||||
|
def _command_error(self, iq, session):
|
||||||
|
"""
|
||||||
|
Process an error that occurs during command execution.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The iq stanza containing the error.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
logging.error("COMMAND: %s %s" % (iq['error']['condition'],
|
||||||
|
iq['error']['text']))
|
||||||
|
|
||||||
|
# Terminate the command's execution and clear its session.
|
||||||
|
# The session will automatically be cleared if no error
|
||||||
|
# handler is provided.
|
||||||
|
self['xep_0050'].terminate_command(session)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Setup the command line arguments.
|
||||||
|
optp = OptionParser()
|
||||||
|
|
||||||
|
# Output verbosity options.
|
||||||
|
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.ERROR, default=logging.INFO)
|
||||||
|
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.DEBUG, default=logging.INFO)
|
||||||
|
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=5, default=logging.INFO)
|
||||||
|
|
||||||
|
# JID and password options.
|
||||||
|
optp.add_option("-j", "--jid", dest="jid",
|
||||||
|
help="JID to use")
|
||||||
|
optp.add_option("-p", "--password", dest="password",
|
||||||
|
help="password to use")
|
||||||
|
optp.add_option("-o", "--other", dest="other",
|
||||||
|
help="JID providing commands")
|
||||||
|
optp.add_option("-g", "--greeting", dest="greeting",
|
||||||
|
help="Greeting")
|
||||||
|
|
||||||
|
opts, args = optp.parse_args()
|
||||||
|
|
||||||
|
# Setup logging.
|
||||||
|
logging.basicConfig(level=opts.loglevel,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
if opts.jid is None:
|
||||||
|
opts.jid = raw_input("Username: ")
|
||||||
|
if opts.password is None:
|
||||||
|
opts.password = getpass.getpass("Password: ")
|
||||||
|
if opts.other is None:
|
||||||
|
opts.other = raw_input("JID Providing Commands: ")
|
||||||
|
if opts.greeting is None:
|
||||||
|
opts.other = raw_input("Greeting: ")
|
||||||
|
|
||||||
|
# Setup the CommandBot and register plugins. Note that while plugins may
|
||||||
|
# have interdependencies, the order in which you register them does
|
||||||
|
# not matter.
|
||||||
|
xmpp = CommandUserBot(opts.jid, opts.password, opts.other, opts.greeting)
|
||||||
|
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||||
|
xmpp.register_plugin('xep_0004') # Data Forms
|
||||||
|
xmpp.register_plugin('xep_0050') # Adhoc Commands
|
||||||
|
|
||||||
|
# If you are working with an OpenFire server, you may need
|
||||||
|
# to adjust the SSL version used:
|
||||||
|
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||||
|
|
||||||
|
# If you want to verify the SSL certificates offered by a server:
|
||||||
|
# xmpp.ca_certs = "path/to/ca/cert"
|
||||||
|
|
||||||
|
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||||
|
if xmpp.connect():
|
||||||
|
# If you do not have the pydns library installed, you will need
|
||||||
|
# to manually specify the name of the server if it does not match
|
||||||
|
# the one in the JID. For example, to use Google Talk you would
|
||||||
|
# need to use:
|
||||||
|
#
|
||||||
|
# if xmpp.connect(('talk.google.com', 5222)):
|
||||||
|
# ...
|
||||||
|
xmpp.process(threaded=False)
|
||||||
|
print("Done")
|
||||||
|
else:
|
||||||
|
print("Unable to connect.")
|
||||||
198
examples/disco_browser.py
Executable file
198
examples/disco_browser.py
Executable file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import getpass
|
||||||
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
|
||||||
|
|
||||||
|
# Python versions before 3.0 do not use UTF-8 encoding
|
||||||
|
# by default. To ensure that Unicode is handled properly
|
||||||
|
# throughout SleekXMPP, we will set the default encoding
|
||||||
|
# ourselves to UTF-8.
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
reload(sys)
|
||||||
|
sys.setdefaultencoding('utf8')
|
||||||
|
|
||||||
|
|
||||||
|
class Disco(sleekxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A demonstration for using basic service discovery.
|
||||||
|
|
||||||
|
Send a disco#info and disco#items request to a JID/node combination,
|
||||||
|
and print out the results.
|
||||||
|
|
||||||
|
May also request only particular info categories such as just features,
|
||||||
|
or just items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, jid, password, target_jid, target_node='', get=''):
|
||||||
|
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||||
|
|
||||||
|
# Using service discovery requires the XEP-0030 plugin.
|
||||||
|
self.register_plugin('xep_0030')
|
||||||
|
|
||||||
|
self.get = get
|
||||||
|
self.target_jid = target_jid
|
||||||
|
self.target_node = target_node
|
||||||
|
|
||||||
|
# Values to control which disco entities are reported
|
||||||
|
self.info_types = ['', 'all', 'info', 'identities', 'features']
|
||||||
|
self.identity_types = ['', 'all', 'info', 'identities']
|
||||||
|
self.feature_types = ['', 'all', 'info', 'features']
|
||||||
|
self.items_types = ['', 'all', 'items']
|
||||||
|
|
||||||
|
|
||||||
|
# The session_start event will be triggered when
|
||||||
|
# the bot establishes its connection with the server
|
||||||
|
# and the XML streams are ready for use. We want to
|
||||||
|
# listen for this event so that we we can intialize
|
||||||
|
# our roster.
|
||||||
|
self.add_event_handler("session_start", self.start)
|
||||||
|
|
||||||
|
def start(self, event):
|
||||||
|
"""
|
||||||
|
Process the session_start event.
|
||||||
|
|
||||||
|
Typical actions for the session_start event are
|
||||||
|
requesting the roster and broadcasting an intial
|
||||||
|
presence stanza.
|
||||||
|
|
||||||
|
In this case, we send disco#info and disco#items
|
||||||
|
stanzas to the requested JID and print the results.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
event -- An empty dictionary. The session_start
|
||||||
|
event does not provide any additional
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.get_roster()
|
||||||
|
self.send_presence()
|
||||||
|
|
||||||
|
if self.get in self.info_types:
|
||||||
|
# By using block=True, the result stanza will be
|
||||||
|
# returned. Execution will block until the reply is
|
||||||
|
# received. Non-blocking options would be to listen
|
||||||
|
# for the disco_info event, or passing a handler
|
||||||
|
# function using the callback parameter.
|
||||||
|
info = self['xep_0030'].get_info(jid=self.target_jid,
|
||||||
|
node=self.target_node,
|
||||||
|
block=True)
|
||||||
|
if self.get in self.items_types:
|
||||||
|
# The same applies from above. Listen for the
|
||||||
|
# disco_items event or pass a callback function
|
||||||
|
# if you need to process a non-blocking request.
|
||||||
|
items = self['xep_0030'].get_items(jid=self.target_jid,
|
||||||
|
node=self.target_node,
|
||||||
|
block=True)
|
||||||
|
else:
|
||||||
|
logging.error("Invalid disco request type.")
|
||||||
|
self.disconnect()
|
||||||
|
return
|
||||||
|
|
||||||
|
header = 'XMPP Service Discovery: %s' % self.target_jid
|
||||||
|
print(header)
|
||||||
|
print('-' * len(header))
|
||||||
|
if self.target_node != '':
|
||||||
|
print('Node: %s' % self.target_node)
|
||||||
|
print('-' * len(header))
|
||||||
|
|
||||||
|
if self.get in self.identity_types:
|
||||||
|
print('Identities:')
|
||||||
|
for identity in info['disco_info']['identities']:
|
||||||
|
print(' - %s' % str(identity))
|
||||||
|
|
||||||
|
if self.get in self.feature_types:
|
||||||
|
print('Features:')
|
||||||
|
for feature in info['disco_info']['features']:
|
||||||
|
print(' - %s' % feature)
|
||||||
|
|
||||||
|
if self.get in self.items_types:
|
||||||
|
print('Items:')
|
||||||
|
for item in items['disco_items']['items']:
|
||||||
|
print(' - %s' % str(item))
|
||||||
|
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Setup the command line arguments.
|
||||||
|
optp = OptionParser()
|
||||||
|
optp.version = '%%prog 0.1'
|
||||||
|
optp.usage = "Usage: %%prog [options] %s <jid> [<node>]" % \
|
||||||
|
'all|info|items|identities|features'
|
||||||
|
|
||||||
|
optp.add_option('-q','--quiet', help='set logging to ERROR',
|
||||||
|
action='store_const',
|
||||||
|
dest='loglevel',
|
||||||
|
const=logging.ERROR,
|
||||||
|
default=logging.ERROR)
|
||||||
|
optp.add_option('-d','--debug', help='set logging to DEBUG',
|
||||||
|
action='store_const',
|
||||||
|
dest='loglevel',
|
||||||
|
const=logging.DEBUG,
|
||||||
|
default=logging.ERROR)
|
||||||
|
optp.add_option('-v','--verbose', help='set logging to COMM',
|
||||||
|
action='store_const',
|
||||||
|
dest='loglevel',
|
||||||
|
const=5,
|
||||||
|
default=logging.ERROR)
|
||||||
|
|
||||||
|
# JID and password options.
|
||||||
|
optp.add_option("-j", "--jid", dest="jid",
|
||||||
|
help="JID to use")
|
||||||
|
optp.add_option("-p", "--password", dest="password",
|
||||||
|
help="password to use")
|
||||||
|
opts,args = optp.parse_args()
|
||||||
|
|
||||||
|
# Setup logging.
|
||||||
|
logging.basicConfig(level=opts.loglevel,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
if len(args) < 2:
|
||||||
|
optp.print_help()
|
||||||
|
exit()
|
||||||
|
|
||||||
|
if len(args) == 2:
|
||||||
|
args = (args[0], args[1], '')
|
||||||
|
|
||||||
|
if opts.jid is None:
|
||||||
|
opts.jid = raw_input("Username: ")
|
||||||
|
if opts.password is None:
|
||||||
|
opts.password = getpass.getpass("Password: ")
|
||||||
|
|
||||||
|
# Setup the Disco browser.
|
||||||
|
xmpp = Disco(opts.jid, opts.password, args[1], args[2], args[0])
|
||||||
|
|
||||||
|
# If you are working with an OpenFire server, you may need
|
||||||
|
# to adjust the SSL version used:
|
||||||
|
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||||
|
|
||||||
|
# If you want to verify the SSL certificates offered by a server:
|
||||||
|
# xmpp.ca_certs = "path/to/ca/cert"
|
||||||
|
|
||||||
|
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||||
|
if xmpp.connect():
|
||||||
|
# If you do not have the pydns library installed, you will need
|
||||||
|
# to manually specify the name of the server if it does not match
|
||||||
|
# the one in the JID. For example, to use Google Talk you would
|
||||||
|
# need to use:
|
||||||
|
#
|
||||||
|
# if xmpp.connect(('talk.google.com', 5222)):
|
||||||
|
# ...
|
||||||
|
xmpp.process(threaded=False)
|
||||||
|
else:
|
||||||
|
print("Unable to connect.")
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
import getpass
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
|
||||||
import sleekxmpp
|
import sleekxmpp
|
||||||
@@ -60,8 +61,8 @@ class EchoBot(sleekxmpp.ClientXMPP):
|
|||||||
event does not provide any additional
|
event does not provide any additional
|
||||||
data.
|
data.
|
||||||
"""
|
"""
|
||||||
self.getRoster()
|
self.send_presence()
|
||||||
self.sendPresence()
|
self.get_roster()
|
||||||
|
|
||||||
def message(self, msg):
|
def message(self, msg):
|
||||||
"""
|
"""
|
||||||
@@ -105,14 +106,26 @@ if __name__ == '__main__':
|
|||||||
logging.basicConfig(level=opts.loglevel,
|
logging.basicConfig(level=opts.loglevel,
|
||||||
format='%(levelname)-8s %(message)s')
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
if opts.jid is None:
|
||||||
|
opts.jid = raw_input("Username: ")
|
||||||
|
if opts.password is None:
|
||||||
|
opts.password = getpass.getpass("Password: ")
|
||||||
|
|
||||||
# Setup the EchoBot and register plugins. Note that while plugins may
|
# Setup the EchoBot and register plugins. Note that while plugins may
|
||||||
# have interdependencies, the order in which you register them does
|
# have interdependencies, the order in which you register them does
|
||||||
# not matter.
|
# not matter.
|
||||||
xmpp = EchoBot(opts.jid, opts.password)
|
xmpp = EchoBot(opts.jid, opts.password)
|
||||||
xmpp.registerPlugin('xep_0030') # Service Discovery
|
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||||
xmpp.registerPlugin('xep_0004') # Data Forms
|
xmpp.register_plugin('xep_0004') # Data Forms
|
||||||
xmpp.registerPlugin('xep_0060') # PubSub
|
xmpp.register_plugin('xep_0060') # PubSub
|
||||||
xmpp.registerPlugin('xep_0199') # XMPP Ping
|
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||||
|
|
||||||
|
# If you are working with an OpenFire server, you may need
|
||||||
|
# to adjust the SSL version used:
|
||||||
|
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||||
|
|
||||||
|
# If you want to verify the SSL certificates offered by a server:
|
||||||
|
# xmpp.ca_certs = "path/to/ca/cert"
|
||||||
|
|
||||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||||
if xmpp.connect():
|
if xmpp.connect():
|
||||||
|
|||||||
186
examples/muc.py
Executable file
186
examples/muc.py
Executable file
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
|
||||||
|
# Python versions before 3.0 do not use UTF-8 encoding
|
||||||
|
# by default. To ensure that Unicode is handled properly
|
||||||
|
# throughout SleekXMPP, we will set the default encoding
|
||||||
|
# ourselves to UTF-8.
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
reload(sys)
|
||||||
|
sys.setdefaultencoding('utf8')
|
||||||
|
|
||||||
|
|
||||||
|
class MUCBot(sleekxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A simple SleekXMPP bot that will greets those
|
||||||
|
who enter the room, and acknowledge any messages
|
||||||
|
that mentions the bot's nickname.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, jid, password, room, nick):
|
||||||
|
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||||
|
|
||||||
|
self.room = room
|
||||||
|
self.nick = nick
|
||||||
|
|
||||||
|
# The session_start event will be triggered when
|
||||||
|
# the bot establishes its connection with the server
|
||||||
|
# and the XML streams are ready for use. We want to
|
||||||
|
# listen for this event so that we we can intialize
|
||||||
|
# our roster.
|
||||||
|
self.add_event_handler("session_start", self.start)
|
||||||
|
|
||||||
|
# The groupchat_message event is triggered whenever a message
|
||||||
|
# stanza is received from any chat room. If you also also
|
||||||
|
# register a handler for the 'message' event, MUC messages
|
||||||
|
# will be processed by both handlers.
|
||||||
|
self.add_event_handler("groupchat_message", self.muc_message)
|
||||||
|
|
||||||
|
# The groupchat_presence event is triggered whenever a
|
||||||
|
# presence stanza is received from any chat room, including
|
||||||
|
# any presences you send yourself. To limit event handling
|
||||||
|
# to a single room, use the events muc::room@server::presence,
|
||||||
|
# muc::room@server::got_online, or muc::room@server::got_offline.
|
||||||
|
self.add_event_handler("muc::%s::got_online" % self.room,
|
||||||
|
self.muc_online)
|
||||||
|
|
||||||
|
|
||||||
|
def start(self, event):
|
||||||
|
"""
|
||||||
|
Process the session_start event.
|
||||||
|
|
||||||
|
Typical actions for the session_start event are
|
||||||
|
requesting the roster and broadcasting an intial
|
||||||
|
presence stanza.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
event -- An empty dictionary. The session_start
|
||||||
|
event does not provide any additional
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.getRoster()
|
||||||
|
self.sendPresence()
|
||||||
|
self.plugin['xep_0045'].joinMUC(self.room,
|
||||||
|
self.nick,
|
||||||
|
# If a room password is needed, use:
|
||||||
|
# password=the_room_password,
|
||||||
|
wait=True)
|
||||||
|
|
||||||
|
def muc_message(self, msg):
|
||||||
|
"""
|
||||||
|
Process incoming message stanzas from any chat room. Be aware
|
||||||
|
that if you also have any handlers for the 'message' event,
|
||||||
|
message stanzas may be processed by both handlers, so check
|
||||||
|
the 'type' attribute when using a 'message' event handler.
|
||||||
|
|
||||||
|
Whenever the bot's nickname is mentioned, respond to
|
||||||
|
the message.
|
||||||
|
|
||||||
|
IMPORTANT: Always check that a message is not from yourself,
|
||||||
|
otherwise you will create an infinite loop responding
|
||||||
|
to your own messages.
|
||||||
|
|
||||||
|
This handler will reply to messages that mention
|
||||||
|
the bot's nickname.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg -- The received message stanza. See the documentation
|
||||||
|
for stanza objects and the Message stanza to see
|
||||||
|
how it may be used.
|
||||||
|
"""
|
||||||
|
if msg['mucnick'] != self.nick and self.nick in msg['body']:
|
||||||
|
self.send_message(mto=msg['from'].bare,
|
||||||
|
mbody="I heard that, %s." % msg['mucnick'],
|
||||||
|
mtype='groupchat')
|
||||||
|
|
||||||
|
def muc_online(self, presence):
|
||||||
|
"""
|
||||||
|
Process a presence stanza from a chat room. In this case,
|
||||||
|
presences from users that have just come online are
|
||||||
|
handled by sending a welcome message that includes
|
||||||
|
the user's nickname and role in the room.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
presence -- The received presence stanza. See the
|
||||||
|
documentation for the Presence stanza
|
||||||
|
to see how else it may be used.
|
||||||
|
"""
|
||||||
|
if presence['muc']['nick'] != self.nick:
|
||||||
|
self.send_message(mto=presence['from'].bare,
|
||||||
|
mbody="Hello, %s %s" % (presence['muc']['role'],
|
||||||
|
presence['muc']['nick']),
|
||||||
|
mtype='groupchat')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Setup the command line arguments.
|
||||||
|
optp = OptionParser()
|
||||||
|
|
||||||
|
# Output verbosity options.
|
||||||
|
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.ERROR, default=logging.INFO)
|
||||||
|
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.DEBUG, default=logging.INFO)
|
||||||
|
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=5, default=logging.INFO)
|
||||||
|
|
||||||
|
# JID and password options.
|
||||||
|
optp.add_option("-j", "--jid", dest="jid",
|
||||||
|
help="JID to use")
|
||||||
|
optp.add_option("-p", "--password", dest="password",
|
||||||
|
help="password to use")
|
||||||
|
optp.add_option("-r", "--room", dest="room",
|
||||||
|
help="MUC room to join")
|
||||||
|
optp.add_option("-n", "--nick", dest="nick",
|
||||||
|
help="MUC nickname")
|
||||||
|
|
||||||
|
opts, args = optp.parse_args()
|
||||||
|
|
||||||
|
# Setup logging.
|
||||||
|
logging.basicConfig(level=opts.loglevel,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
if None in [opts.jid, opts.password, opts.room, opts.nick]:
|
||||||
|
optp.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Setup the MUCBot and register plugins. Note that while plugins may
|
||||||
|
# have interdependencies, the order in which you register them does
|
||||||
|
# not matter.
|
||||||
|
xmpp = MUCBot(opts.jid, opts.password, opts.room, opts.nick)
|
||||||
|
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||||
|
xmpp.register_plugin('xep_0045') # Multi-User Chat
|
||||||
|
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||||
|
|
||||||
|
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||||
|
if xmpp.connect():
|
||||||
|
# If you do not have the pydns library installed, you will need
|
||||||
|
# to manually specify the name of the server if it does not match
|
||||||
|
# the one in the JID. For example, to use Google Talk you would
|
||||||
|
# need to use:
|
||||||
|
#
|
||||||
|
# if xmpp.connect(('talk.google.com', 5222)):
|
||||||
|
# ...
|
||||||
|
xmpp.process(threaded=False)
|
||||||
|
print("Done")
|
||||||
|
else:
|
||||||
|
print("Unable to connect.")
|
||||||
140
examples/ping.py
Executable file
140
examples/ping.py
Executable file
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import getpass
|
||||||
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
|
||||||
|
# Python versions before 3.0 do not use UTF-8 encoding
|
||||||
|
# by default. To ensure that Unicode is handled properly
|
||||||
|
# throughout SleekXMPP, we will set the default encoding
|
||||||
|
# ourselves to UTF-8.
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
reload(sys)
|
||||||
|
sys.setdefaultencoding('utf8')
|
||||||
|
|
||||||
|
|
||||||
|
class PingTest(sleekxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A simple SleekXMPP bot that will send a ping request
|
||||||
|
to a given JID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, jid, password, pingjid):
|
||||||
|
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||||
|
if pingjid is None:
|
||||||
|
pingjid = self.jid
|
||||||
|
self.pingjid = pingjid
|
||||||
|
|
||||||
|
# The session_start event will be triggered when
|
||||||
|
# the bot establishes its connection with the server
|
||||||
|
# and the XML streams are ready for use. We want to
|
||||||
|
# listen for this event so that we we can intialize
|
||||||
|
# our roster.
|
||||||
|
self.add_event_handler("session_start", self.start)
|
||||||
|
|
||||||
|
def start(self, event):
|
||||||
|
"""
|
||||||
|
Process the session_start event.
|
||||||
|
|
||||||
|
Typical actions for the session_start event are
|
||||||
|
requesting the roster and broadcasting an intial
|
||||||
|
presence stanza.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
event -- An empty dictionary. The session_start
|
||||||
|
event does not provide any additional
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.send_presence()
|
||||||
|
self.get_roster()
|
||||||
|
result = self['xep_0199'].send_ping(self.pingjid,
|
||||||
|
timeout=10,
|
||||||
|
errorfalse=True)
|
||||||
|
logging.info("Pinging...")
|
||||||
|
if result is False:
|
||||||
|
logging.info("Couldn't ping.")
|
||||||
|
self.disconnect()
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
logging.info("Success! RTT: %s" % str(result))
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Setup the command line arguments.
|
||||||
|
optp = OptionParser()
|
||||||
|
|
||||||
|
# Output verbosity options.
|
||||||
|
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.ERROR, default=logging.INFO)
|
||||||
|
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.DEBUG, default=logging.INFO)
|
||||||
|
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=5, default=logging.INFO)
|
||||||
|
optp.add_option('-t', '--pingto', help='set jid to ping',
|
||||||
|
action='store', type='string', dest='pingjid',
|
||||||
|
default=None)
|
||||||
|
|
||||||
|
# JID and password options.
|
||||||
|
optp.add_option("-j", "--jid", dest="jid",
|
||||||
|
help="JID to use")
|
||||||
|
optp.add_option("-p", "--password", dest="password",
|
||||||
|
help="password to use")
|
||||||
|
|
||||||
|
opts, args = optp.parse_args()
|
||||||
|
|
||||||
|
# Setup logging.
|
||||||
|
logging.basicConfig(level=opts.loglevel,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
if opts.jid is None:
|
||||||
|
opts.jid = raw_input("Username: ")
|
||||||
|
if opts.password is None:
|
||||||
|
opts.password = getpass.getpass("Password: ")
|
||||||
|
|
||||||
|
# Setup the PingTest and register plugins. Note that while plugins may
|
||||||
|
# have interdependencies, the order in which you register them does
|
||||||
|
# not matter.
|
||||||
|
xmpp = PingTest(opts.jid, opts.password, opts.pingjid)
|
||||||
|
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||||
|
xmpp.register_plugin('xep_0004') # Data Forms
|
||||||
|
xmpp.register_plugin('xep_0060') # PubSub
|
||||||
|
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||||
|
|
||||||
|
# If you are working with an OpenFire server, you may need
|
||||||
|
# to adjust the SSL version used:
|
||||||
|
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||||
|
|
||||||
|
# If you want to verify the SSL certificates offered by a server:
|
||||||
|
# xmpp.ca_certs = "path/to/ca/cert"
|
||||||
|
|
||||||
|
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||||
|
if xmpp.connect():
|
||||||
|
# If you do not have the pydns library installed, you will need
|
||||||
|
# to manually specify the name of the server if it does not match
|
||||||
|
# the one in the JID. For example, to use Google Talk you would
|
||||||
|
# need to use:
|
||||||
|
#
|
||||||
|
# if xmpp.connect(('talk.google.com', 5222)):
|
||||||
|
# ...
|
||||||
|
xmpp.process(threaded=False)
|
||||||
|
print("Done")
|
||||||
|
else:
|
||||||
|
print("Unable to connect.")
|
||||||
44
examples/rpc_async.py
Normal file
44
examples/rpc_async.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Dann Martens
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \
|
||||||
|
ANY_ALL, Future
|
||||||
|
import time
|
||||||
|
|
||||||
|
class Boomerang(Endpoint):
|
||||||
|
|
||||||
|
def FQN(self):
|
||||||
|
return 'boomerang'
|
||||||
|
|
||||||
|
@remote
|
||||||
|
def throw(self):
|
||||||
|
print "Duck!"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
session = Remote.new_session('kangaroo@xmpp.org/rpc', '*****')
|
||||||
|
|
||||||
|
session.new_handler(ANY_ALL, Boomerang)
|
||||||
|
|
||||||
|
boomerang = session.new_proxy('kangaroo@xmpp.org/rpc', Boomerang)
|
||||||
|
|
||||||
|
callback = Future()
|
||||||
|
|
||||||
|
boomerang.async(callback).throw()
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
53
examples/rpc_client_side.py
Normal file
53
examples/rpc_client_side.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Dann Martens
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \
|
||||||
|
ANY_ALL
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
class Thermostat(Endpoint):
|
||||||
|
|
||||||
|
def FQN(self):
|
||||||
|
return 'thermostat'
|
||||||
|
|
||||||
|
def __init(self, initial_temperature):
|
||||||
|
self._temperature = initial_temperature
|
||||||
|
self._event = threading.Event()
|
||||||
|
|
||||||
|
@remote
|
||||||
|
def set_temperature(self, temperature):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
@remote
|
||||||
|
def get_temperature(self):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
@remote(False)
|
||||||
|
def release(self):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
session = Remote.new_session('operator@xmpp.org/rpc', '*****')
|
||||||
|
|
||||||
|
thermostat = session.new_proxy('thermostat@xmpp.org/rpc', Thermostat)
|
||||||
|
|
||||||
|
print("Current temperature is %s" % thermostat.get_temperature())
|
||||||
|
|
||||||
|
thermostat.set_temperature(20)
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
52
examples/rpc_server_side.py
Normal file
52
examples/rpc_server_side.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Dann Martens
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \
|
||||||
|
ANY_ALL
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class Thermostat(Endpoint):
|
||||||
|
|
||||||
|
def FQN(self):
|
||||||
|
return 'thermostat'
|
||||||
|
|
||||||
|
def __init(self, initial_temperature):
|
||||||
|
self._temperature = initial_temperature
|
||||||
|
self._event = threading.Event()
|
||||||
|
|
||||||
|
@remote
|
||||||
|
def set_temperature(self, temperature):
|
||||||
|
print("Setting temperature to %s" % temperature)
|
||||||
|
self._temperature = temperature
|
||||||
|
|
||||||
|
@remote
|
||||||
|
def get_temperature(self):
|
||||||
|
return self._temperature
|
||||||
|
|
||||||
|
@remote(False)
|
||||||
|
def release(self):
|
||||||
|
self._event.set()
|
||||||
|
|
||||||
|
def wait_for_release(self):
|
||||||
|
self._event.wait()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
session = Remote.new_session('sleek@xmpp.org/rpc', '*****')
|
||||||
|
|
||||||
|
thermostat = session.new_handler(ANY_ALL, Thermostat, 18)
|
||||||
|
|
||||||
|
thermostat.wait_for_release()
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
17
setup.py
17
setup.py
@@ -12,6 +12,8 @@
|
|||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
|
||||||
# if 'cygwin' in sys.platform.lower():
|
# if 'cygwin' in sys.platform.lower():
|
||||||
# min_version = '0.6c6'
|
# min_version = '0.6c6'
|
||||||
# else:
|
# else:
|
||||||
@@ -25,7 +27,7 @@ import sys
|
|||||||
#
|
#
|
||||||
# from setuptools import setup, find_packages, Extension, Feature
|
# from setuptools import setup, find_packages, Extension, Feature
|
||||||
|
|
||||||
VERSION = '1.0.0.0'
|
VERSION = sleekxmpp.__version__
|
||||||
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
|
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
|
||||||
LONG_DESCRIPTION = """
|
LONG_DESCRIPTION = """
|
||||||
SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).
|
SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).
|
||||||
@@ -38,13 +40,24 @@ CLASSIFIERS = [ 'Intended Audience :: Developers',
|
|||||||
]
|
]
|
||||||
|
|
||||||
packages = [ 'sleekxmpp',
|
packages = [ 'sleekxmpp',
|
||||||
'sleekxmpp/plugins',
|
|
||||||
'sleekxmpp/stanza',
|
'sleekxmpp/stanza',
|
||||||
'sleekxmpp/test',
|
'sleekxmpp/test',
|
||||||
'sleekxmpp/xmlstream',
|
'sleekxmpp/xmlstream',
|
||||||
'sleekxmpp/xmlstream/matcher',
|
'sleekxmpp/xmlstream/matcher',
|
||||||
'sleekxmpp/xmlstream/handler',
|
'sleekxmpp/xmlstream/handler',
|
||||||
'sleekxmpp/thirdparty',
|
'sleekxmpp/thirdparty',
|
||||||
|
'sleekxmpp/plugins',
|
||||||
|
'sleekxmpp/plugins/xep_0009',
|
||||||
|
'sleekxmpp/plugins/xep_0009/stanza',
|
||||||
|
'sleekxmpp/plugins/xep_0030',
|
||||||
|
'sleekxmpp/plugins/xep_0030/stanza',
|
||||||
|
'sleekxmpp/plugins/xep_0050',
|
||||||
|
'sleekxmpp/plugins/xep_0059',
|
||||||
|
'sleekxmpp/plugins/xep_0085',
|
||||||
|
'sleekxmpp/plugins/xep_0086',
|
||||||
|
'sleekxmpp/plugins/xep_0092',
|
||||||
|
'sleekxmpp/plugins/xep_0128',
|
||||||
|
'sleekxmpp/plugins/xep_0199',
|
||||||
]
|
]
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
|
|||||||
@@ -14,3 +14,6 @@ from sleekxmpp.xmlstream.handler import *
|
|||||||
from sleekxmpp.xmlstream import XMLStream, RestartStream
|
from sleekxmpp.xmlstream import XMLStream, RestartStream
|
||||||
from sleekxmpp.xmlstream.matcher import *
|
from sleekxmpp.xmlstream.matcher import *
|
||||||
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
||||||
|
|
||||||
|
__version__ = '1.0beta5'
|
||||||
|
__version_info__ = (1, 0, 0, 'beta5', 0)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import logging
|
|||||||
import sleekxmpp
|
import sleekxmpp
|
||||||
from sleekxmpp import plugins
|
from sleekxmpp import plugins
|
||||||
|
|
||||||
from sleekxmpp.stanza import Message, Presence, Iq, Error
|
from sleekxmpp.stanza import Message, Presence, Iq, Error, StreamError
|
||||||
from sleekxmpp.stanza.roster import Roster
|
from sleekxmpp.stanza.roster import Roster
|
||||||
from sleekxmpp.stanza.nick import Nick
|
from sleekxmpp.stanza.nick import Nick
|
||||||
from sleekxmpp.stanza.htmlim import HTMLIM
|
from sleekxmpp.stanza.htmlim import HTMLIM
|
||||||
@@ -90,26 +90,14 @@ class BaseXMPP(XMLStream):
|
|||||||
|
|
||||||
# To comply with PEP8, method names now use underscores.
|
# To comply with PEP8, method names now use underscores.
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
self.registerPlugin = self.register_plugin
|
|
||||||
self.makeIq = self.make_iq
|
|
||||||
self.makeIqGet = self.make_iq_get
|
|
||||||
self.makeIqResult = self.make_iq_result
|
|
||||||
self.makeIqSet = self.make_iq_set
|
|
||||||
self.makeIqError = self.make_iq_error
|
|
||||||
self.makeIqQuery = self.make_iq_query
|
|
||||||
self.makeQueryRoster = self.make_query_roster
|
|
||||||
self.makeMessage = self.make_message
|
|
||||||
self.makePresence = self.make_presence
|
|
||||||
self.sendMessage = self.send_message
|
|
||||||
self.sendPresence = self.send_presence
|
|
||||||
self.sendPresenceSubscription = self.send_presence_subscription
|
|
||||||
|
|
||||||
self.default_ns = default_ns
|
self.default_ns = default_ns
|
||||||
self.stream_ns = 'http://etherx.jabber.org/streams'
|
self.stream_ns = 'http://etherx.jabber.org/streams'
|
||||||
|
|
||||||
self.boundjid = JID("")
|
self.boundjid = JID("")
|
||||||
|
|
||||||
self.plugin = {}
|
self.plugin = {}
|
||||||
|
self.plugin_config = {}
|
||||||
|
self.plugin_whitelist = []
|
||||||
self.roster = {}
|
self.roster = {}
|
||||||
self.is_component = False
|
self.is_component = False
|
||||||
self.auto_authorize = True
|
self.auto_authorize = True
|
||||||
@@ -126,6 +114,10 @@ class BaseXMPP(XMLStream):
|
|||||||
Callback('Presence',
|
Callback('Presence',
|
||||||
MatchXPath("{%s}presence" % self.default_ns),
|
MatchXPath("{%s}presence" % self.default_ns),
|
||||||
self._handle_presence))
|
self._handle_presence))
|
||||||
|
self.register_handler(
|
||||||
|
Callback('Stream Error',
|
||||||
|
MatchXPath("{%s}error" % self.stream_ns),
|
||||||
|
self._handle_stream_error))
|
||||||
|
|
||||||
self.add_event_handler('presence_subscribe',
|
self.add_event_handler('presence_subscribe',
|
||||||
self._handle_subscribe)
|
self._handle_subscribe)
|
||||||
@@ -133,9 +125,10 @@ class BaseXMPP(XMLStream):
|
|||||||
self._handle_disconnected)
|
self._handle_disconnected)
|
||||||
|
|
||||||
# Set up the XML stream with XMPP's root stanzas.
|
# Set up the XML stream with XMPP's root stanzas.
|
||||||
self.registerStanza(Message)
|
self.register_stanza(Message)
|
||||||
self.registerStanza(Iq)
|
self.register_stanza(Iq)
|
||||||
self.registerStanza(Presence)
|
self.register_stanza(Presence)
|
||||||
|
self.register_stanza(StreamError)
|
||||||
|
|
||||||
# Initialize a few default stanza plugins.
|
# Initialize a few default stanza plugins.
|
||||||
register_stanza_plugin(Iq, Roster)
|
register_stanza_plugin(Iq, Roster)
|
||||||
@@ -243,19 +236,27 @@ class BaseXMPP(XMLStream):
|
|||||||
"""Create a Presence stanza associated with this stream."""
|
"""Create a Presence stanza associated with this stream."""
|
||||||
return Presence(self, *args, **kwargs)
|
return Presence(self, *args, **kwargs)
|
||||||
|
|
||||||
def make_iq(self, id=0, ifrom=None):
|
def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None):
|
||||||
"""
|
"""
|
||||||
Create a new Iq stanza with a given Id and from JID.
|
Create a new Iq stanza with a given Id and from JID.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
id -- An ideally unique ID value for this stanza thread.
|
id -- An ideally unique ID value for this stanza thread.
|
||||||
Defaults to 0.
|
Defaults to 0.
|
||||||
ifrom -- The from JID to use for this stanza.
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
itype -- The Iq's type, one of: get, set, result, or error.
|
||||||
|
iquery -- Optional namespace for adding a query element.
|
||||||
"""
|
"""
|
||||||
return self.Iq()._set_stanza_values({'id': str(id),
|
iq = self.Iq()
|
||||||
'from': ifrom})
|
iq['id'] = str(id)
|
||||||
|
iq['to'] = ito
|
||||||
|
iq['from'] = ifrom
|
||||||
|
iq['type'] = itype
|
||||||
|
iq['query'] = iquery
|
||||||
|
return iq
|
||||||
|
|
||||||
def make_iq_get(self, queryxmlns=None):
|
def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None):
|
||||||
"""
|
"""
|
||||||
Create an Iq stanza of type 'get'.
|
Create an Iq stanza of type 'get'.
|
||||||
|
|
||||||
@@ -263,21 +264,45 @@ class BaseXMPP(XMLStream):
|
|||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
queryxmlns -- The namespace of the query to use.
|
queryxmlns -- The namespace of the query to use.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
iq -- Optionally use an existing stanza instead
|
||||||
|
of generating a new one.
|
||||||
"""
|
"""
|
||||||
return self.Iq()._set_stanza_values({'type': 'get',
|
if not iq:
|
||||||
'query': queryxmlns})
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['query'] = queryxmlns
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
return iq
|
||||||
|
|
||||||
def make_iq_result(self, id):
|
def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
|
||||||
"""
|
"""
|
||||||
Create an Iq stanza of type 'result' with the given ID value.
|
Create an Iq stanza of type 'result' with the given ID value.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
id -- An ideally unique ID value. May use self.new_id().
|
id -- An ideally unique ID value. May use self.new_id().
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
iq -- Optionally use an existing stanza instead
|
||||||
|
of generating a new one.
|
||||||
"""
|
"""
|
||||||
return self.Iq()._set_stanza_values({'id': id,
|
if not iq:
|
||||||
'type': 'result'})
|
iq = self.Iq()
|
||||||
|
if id is None:
|
||||||
|
id = self.new_id()
|
||||||
|
iq['id'] = id
|
||||||
|
iq['type'] = 'result'
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
return iq
|
||||||
|
|
||||||
def make_iq_set(self, sub=None):
|
def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None):
|
||||||
"""
|
"""
|
||||||
Create an Iq stanza of type 'set'.
|
Create an Iq stanza of type 'set'.
|
||||||
|
|
||||||
@@ -285,15 +310,26 @@ class BaseXMPP(XMLStream):
|
|||||||
stanza's payload.
|
stanza's payload.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
sub -- A stanza or XML object to use as the Iq's payload.
|
sub -- A stanza or XML object to use as the Iq's payload.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
iq -- Optionally use an existing stanza instead
|
||||||
|
of generating a new one.
|
||||||
"""
|
"""
|
||||||
iq = self.Iq()._set_stanza_values({'type': 'set'})
|
if not iq:
|
||||||
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
if sub != None:
|
if sub != None:
|
||||||
iq.append(sub)
|
iq.append(sub)
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
return iq
|
return iq
|
||||||
|
|
||||||
def make_iq_error(self, id, type='cancel',
|
def make_iq_error(self, id, type='cancel',
|
||||||
condition='feature-not-implemented', text=None):
|
condition='feature-not-implemented',
|
||||||
|
text=None, ito=None, ifrom=None, iq=None):
|
||||||
"""
|
"""
|
||||||
Create an Iq stanza of type 'error'.
|
Create an Iq stanza of type 'error'.
|
||||||
|
|
||||||
@@ -304,14 +340,24 @@ class BaseXMPP(XMLStream):
|
|||||||
condition -- The error condition.
|
condition -- The error condition.
|
||||||
Defaults to 'feature-not-implemented'.
|
Defaults to 'feature-not-implemented'.
|
||||||
text -- A message describing the cause of the error.
|
text -- A message describing the cause of the error.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
iq -- Optionally use an existing stanza instead
|
||||||
|
of generating a new one.
|
||||||
"""
|
"""
|
||||||
iq = self.Iq()._set_stanza_values({'id': id})
|
if not iq:
|
||||||
iq['error']._set_stanza_values({'type': type,
|
iq = self.Iq()
|
||||||
'condition': condition,
|
iq['id'] = id
|
||||||
'text': text})
|
iq['error']['type'] = type
|
||||||
|
iq['error']['condition'] = condition
|
||||||
|
iq['error']['text'] = text
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
return iq
|
return iq
|
||||||
|
|
||||||
def make_iq_query(self, iq=None, xmlns=''):
|
def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
|
||||||
"""
|
"""
|
||||||
Create or modify an Iq stanza to use the given
|
Create or modify an Iq stanza to use the given
|
||||||
query namespace.
|
query namespace.
|
||||||
@@ -320,10 +366,16 @@ class BaseXMPP(XMLStream):
|
|||||||
iq -- Optional Iq stanza to modify. A new
|
iq -- Optional Iq stanza to modify. A new
|
||||||
stanza is created otherwise.
|
stanza is created otherwise.
|
||||||
xmlns -- The query's namespace.
|
xmlns -- The query's namespace.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
"""
|
"""
|
||||||
if not iq:
|
if not iq:
|
||||||
iq = self.Iq()
|
iq = self.Iq()
|
||||||
iq['query'] = xmlns
|
iq['query'] = xmlns
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
return iq
|
return iq
|
||||||
|
|
||||||
def make_query_roster(self, iq=None):
|
def make_query_roster(self, iq=None):
|
||||||
@@ -518,6 +570,9 @@ class BaseXMPP(XMLStream):
|
|||||||
"""When disconnected, reset the roster"""
|
"""When disconnected, reset the roster"""
|
||||||
self.roster = {}
|
self.roster = {}
|
||||||
|
|
||||||
|
def _handle_stream_error(self, error):
|
||||||
|
self.event('stream_error', error)
|
||||||
|
|
||||||
def _handle_message(self, msg):
|
def _handle_message(self, msg):
|
||||||
"""Process incoming message stanzas."""
|
"""Process incoming message stanzas."""
|
||||||
self.event('message', msg)
|
self.event('message', msg)
|
||||||
@@ -559,7 +614,7 @@ class BaseXMPP(XMLStream):
|
|||||||
'in_roster': False}
|
'in_roster': False}
|
||||||
|
|
||||||
# Alias to simplify some references.
|
# Alias to simplify some references.
|
||||||
connections = self.roster[jid]['presence']
|
connections = self.roster[jid].get('presence', {})
|
||||||
|
|
||||||
# Determine if the user has just come online.
|
# Determine if the user has just come online.
|
||||||
if not resource in connections:
|
if not resource in connections:
|
||||||
@@ -585,7 +640,8 @@ class BaseXMPP(XMLStream):
|
|||||||
log.debug("%s %s got offline" % (jid, resource))
|
log.debug("%s %s got offline" % (jid, resource))
|
||||||
del connections[resource]
|
del connections[resource]
|
||||||
|
|
||||||
if not connections and not self.roster[jid]['in_roster']:
|
if not connections and \
|
||||||
|
not self.roster[jid].get('in_roster', False):
|
||||||
del self.roster[jid]
|
del self.roster[jid]
|
||||||
if not was_offline:
|
if not was_offline:
|
||||||
self.event("got_offline", presence)
|
self.event("got_offline", presence)
|
||||||
@@ -632,3 +688,19 @@ class BaseXMPP(XMLStream):
|
|||||||
|
|
||||||
# Restore the old, lowercased name for backwards compatibility.
|
# Restore the old, lowercased name for backwards compatibility.
|
||||||
basexmpp = BaseXMPP
|
basexmpp = BaseXMPP
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
BaseXMPP.registerPlugin = BaseXMPP.register_plugin
|
||||||
|
BaseXMPP.makeIq = BaseXMPP.make_iq
|
||||||
|
BaseXMPP.makeIqGet = BaseXMPP.make_iq_get
|
||||||
|
BaseXMPP.makeIqResult = BaseXMPP.make_iq_result
|
||||||
|
BaseXMPP.makeIqSet = BaseXMPP.make_iq_set
|
||||||
|
BaseXMPP.makeIqError = BaseXMPP.make_iq_error
|
||||||
|
BaseXMPP.makeIqQuery = BaseXMPP.make_iq_query
|
||||||
|
BaseXMPP.makeQueryRoster = BaseXMPP.make_query_roster
|
||||||
|
BaseXMPP.makeMessage = BaseXMPP.make_message
|
||||||
|
BaseXMPP.makePresence = BaseXMPP.make_presence
|
||||||
|
BaseXMPP.sendMessage = BaseXMPP.send_message
|
||||||
|
BaseXMPP.sendPresence = BaseXMPP.send_presence
|
||||||
|
BaseXMPP.sendPresenceSubscription = BaseXMPP.send_presence_subscription
|
||||||
|
|||||||
@@ -68,13 +68,6 @@ class ClientXMPP(BaseXMPP):
|
|||||||
"""
|
"""
|
||||||
BaseXMPP.__init__(self, 'jabber:client')
|
BaseXMPP.__init__(self, 'jabber:client')
|
||||||
|
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.updateRoster = self.update_roster
|
|
||||||
self.delRosterItem = self.del_roster_item
|
|
||||||
self.getRoster = self.get_roster
|
|
||||||
self.registerFeature = self.register_feature
|
|
||||||
|
|
||||||
self.set_jid(jid)
|
self.set_jid(jid)
|
||||||
self.password = password
|
self.password = password
|
||||||
self.escape_quotes = escape_quotes
|
self.escape_quotes = escape_quotes
|
||||||
@@ -82,9 +75,6 @@ class ClientXMPP(BaseXMPP):
|
|||||||
self.plugin_whitelist = plugin_whitelist
|
self.plugin_whitelist = plugin_whitelist
|
||||||
self.srv_support = SRV_SUPPORT
|
self.srv_support = SRV_SUPPORT
|
||||||
|
|
||||||
self.session_started_event = threading.Event()
|
|
||||||
self.session_started_event.clear()
|
|
||||||
|
|
||||||
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
|
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
|
||||||
self.boundjid.host,
|
self.boundjid.host,
|
||||||
"xmlns:stream='%s'" % self.stream_ns,
|
"xmlns:stream='%s'" % self.stream_ns,
|
||||||
@@ -139,7 +129,7 @@ class ClientXMPP(BaseXMPP):
|
|||||||
log.debug("Session start has taken more than 15 seconds")
|
log.debug("Session start has taken more than 15 seconds")
|
||||||
self.disconnect(reconnect=self.auto_reconnect)
|
self.disconnect(reconnect=self.auto_reconnect)
|
||||||
|
|
||||||
def connect(self, address=tuple()):
|
def connect(self, address=tuple(), reattempt=True, use_tls=True):
|
||||||
"""
|
"""
|
||||||
Connect to the XMPP server.
|
Connect to the XMPP server.
|
||||||
|
|
||||||
@@ -148,7 +138,11 @@ class ClientXMPP(BaseXMPP):
|
|||||||
will be used.
|
will be used.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
address -- A tuple containing the server's host and port.
|
address -- A tuple containing the server's host and port.
|
||||||
|
reattempt -- If True, reattempt the connection if an
|
||||||
|
error occurs. Defaults to True.
|
||||||
|
use_tls -- Indicates if TLS should be used for the
|
||||||
|
connection. Defaults to True.
|
||||||
"""
|
"""
|
||||||
self.session_started_event.clear()
|
self.session_started_event.clear()
|
||||||
if not address or len(address) < 2:
|
if not address or len(address) < 2:
|
||||||
@@ -162,11 +156,13 @@ class ClientXMPP(BaseXMPP):
|
|||||||
log.debug("Since no address is supplied," + \
|
log.debug("Since no address is supplied," + \
|
||||||
"attempting SRV lookup.")
|
"attempting SRV lookup.")
|
||||||
try:
|
try:
|
||||||
xmpp_srv = "_xmpp-client._tcp.%s" % self.server
|
xmpp_srv = "_xmpp-client._tcp.%s" % self.boundjid.host
|
||||||
answers = dns.resolver.query(xmpp_srv, dns.rdatatype.SRV)
|
answers = dns.resolver.query(xmpp_srv, dns.rdatatype.SRV)
|
||||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||||
log.debug("No appropriate SRV record found." + \
|
log.debug("No appropriate SRV record found." + \
|
||||||
" Using JID server name.")
|
" Using JID server name.")
|
||||||
|
except (dns.exception.Timeout,):
|
||||||
|
log.debug("DNS resolution timed out.")
|
||||||
else:
|
else:
|
||||||
# Pick a random server, weighted by priority.
|
# Pick a random server, weighted by priority.
|
||||||
|
|
||||||
@@ -190,7 +186,8 @@ class ClientXMPP(BaseXMPP):
|
|||||||
# If all else fails, use the server from the JID.
|
# If all else fails, use the server from the JID.
|
||||||
address = (self.boundjid.host, 5222)
|
address = (self.boundjid.host, 5222)
|
||||||
|
|
||||||
return XMLStream.connect(self, address[0], address[1], use_tls=True)
|
return XMLStream.connect(self, address[0], address[1],
|
||||||
|
use_tls=use_tls, reattempt=reattempt)
|
||||||
|
|
||||||
def register_feature(self, mask, pointer, breaker=False):
|
def register_feature(self, mask, pointer, breaker=False):
|
||||||
"""
|
"""
|
||||||
@@ -206,7 +203,8 @@ class ClientXMPP(BaseXMPP):
|
|||||||
pointer,
|
pointer,
|
||||||
breaker))
|
breaker))
|
||||||
|
|
||||||
def update_roster(self, jid, name=None, subscription=None, groups=[]):
|
def update_roster(self, jid, name=None, subscription=None, groups=[],
|
||||||
|
block=True, timeout=None, callback=None):
|
||||||
"""
|
"""
|
||||||
Add or change a roster item.
|
Add or change a roster item.
|
||||||
|
|
||||||
@@ -217,12 +215,24 @@ class ClientXMPP(BaseXMPP):
|
|||||||
'to', 'from', 'both', or 'none'. If set
|
'to', 'from', 'both', or 'none'. If set
|
||||||
to 'remove', the entry will be deleted.
|
to 'remove', the entry will be deleted.
|
||||||
groups -- The roster groups that contain this item.
|
groups -- The roster groups that contain this item.
|
||||||
|
block -- Specify if the roster request will block
|
||||||
|
until a response is received, or a timeout
|
||||||
|
occurs. Defaults to True.
|
||||||
|
timeout -- The length of time (in seconds) to wait
|
||||||
|
for a response before continuing if blocking
|
||||||
|
is used. Defaults to self.response_timeout.
|
||||||
|
callback -- Optional reference to a stream handler function.
|
||||||
|
Will be executed when the roster is received.
|
||||||
|
Implies block=False.
|
||||||
"""
|
"""
|
||||||
iq = self.Iq()._set_stanza_values({'type': 'set'})
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
iq['roster']['items'] = {jid: {'name': name,
|
iq['roster']['items'] = {jid: {'name': name,
|
||||||
'subscription': subscription,
|
'subscription': subscription,
|
||||||
'groups': groups}}
|
'groups': groups}}
|
||||||
response = iq.send()
|
response = iq.send(block, timeout, callback)
|
||||||
|
if response in [False, None] or not isinstance(response, Iq):
|
||||||
|
return response
|
||||||
return response['type'] == 'result'
|
return response['type'] == 'result'
|
||||||
|
|
||||||
def del_roster_item(self, jid):
|
def del_roster_item(self, jid):
|
||||||
@@ -235,11 +245,33 @@ class ClientXMPP(BaseXMPP):
|
|||||||
"""
|
"""
|
||||||
return self.update_roster(jid, subscription='remove')
|
return self.update_roster(jid, subscription='remove')
|
||||||
|
|
||||||
def get_roster(self):
|
def get_roster(self, block=True, timeout=None, callback=None):
|
||||||
"""Request the roster from the server."""
|
"""
|
||||||
iq = self.Iq()._set_stanza_values({'type': 'get'}).enable('roster')
|
Request the roster from the server.
|
||||||
response = iq.send()
|
|
||||||
self._handle_roster(response, request=True)
|
Arguments:
|
||||||
|
block -- Specify if the roster request will block until a
|
||||||
|
response is received, or a timeout occurs.
|
||||||
|
Defaults to True.
|
||||||
|
timeout -- The length of time (in seconds) to wait for a response
|
||||||
|
before continuing if blocking is used.
|
||||||
|
Defaults to self.response_timeout.
|
||||||
|
callback -- Optional reference to a stream handler function. Will
|
||||||
|
be executed when the roster is received.
|
||||||
|
Implies block=False.
|
||||||
|
"""
|
||||||
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq.enable('roster')
|
||||||
|
response = iq.send(block, timeout, callback)
|
||||||
|
|
||||||
|
if response == False:
|
||||||
|
self.event('roster_timeout')
|
||||||
|
|
||||||
|
if response in [False, None] or not isinstance(response, Iq):
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
return self._handle_roster(response, request=True)
|
||||||
|
|
||||||
def _handle_stream_features(self, features):
|
def _handle_stream_features(self, features):
|
||||||
"""
|
"""
|
||||||
@@ -270,13 +302,15 @@ class ClientXMPP(BaseXMPP):
|
|||||||
Arguments:
|
Arguments:
|
||||||
xml -- The STARTLS proceed element.
|
xml -- The STARTLS proceed element.
|
||||||
"""
|
"""
|
||||||
if not self.authenticated and self.ssl_support:
|
if not self.use_tls:
|
||||||
|
return False
|
||||||
|
elif not self.authenticated and self.ssl_support:
|
||||||
tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
|
tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
|
||||||
self.add_handler("<proceed xmlns='%s' />" % tls_ns,
|
self.add_handler("<proceed xmlns='%s' />" % tls_ns,
|
||||||
self._handle_tls_start,
|
self._handle_tls_start,
|
||||||
name='TLS Proceed',
|
name='TLS Proceed',
|
||||||
instream=True)
|
instream=True)
|
||||||
self.send_xml(xml)
|
self.send_xml(xml, now=True)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
log.warning("The module tlslite is required to log in" +\
|
log.warning("The module tlslite is required to log in" +\
|
||||||
@@ -300,7 +334,8 @@ class ClientXMPP(BaseXMPP):
|
|||||||
Arguments:
|
Arguments:
|
||||||
xml -- The SASL mechanisms stanza.
|
xml -- The SASL mechanisms stanza.
|
||||||
"""
|
"""
|
||||||
if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
|
if self.use_tls and \
|
||||||
|
'{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
log.debug("Starting SASL Auth")
|
log.debug("Starting SASL Auth")
|
||||||
@@ -331,11 +366,13 @@ class ClientXMPP(BaseXMPP):
|
|||||||
|
|
||||||
self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % (
|
self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % (
|
||||||
sasl_ns,
|
sasl_ns,
|
||||||
auth))
|
auth),
|
||||||
|
now=True)
|
||||||
elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
|
elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
|
||||||
self.send("<auth xmlns='%s' mechanism='%s' />" % (
|
self.send("<auth xmlns='%s' mechanism='%s' />" % (
|
||||||
sasl_ns,
|
sasl_ns,
|
||||||
'ANONYMOUS'))
|
'ANONYMOUS'),
|
||||||
|
now=True)
|
||||||
else:
|
else:
|
||||||
log.error("No appropriate login method.")
|
log.error("No appropriate login method.")
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
@@ -378,13 +415,13 @@ class ClientXMPP(BaseXMPP):
|
|||||||
res.text = self.boundjid.resource
|
res.text = self.boundjid.resource
|
||||||
xml.append(res)
|
xml.append(res)
|
||||||
iq.append(xml)
|
iq.append(xml)
|
||||||
response = iq.send()
|
response = iq.send(now=True)
|
||||||
|
|
||||||
bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
|
bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
|
||||||
self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
|
self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
|
||||||
bind_ns)).text)
|
bind_ns)).text)
|
||||||
self.bound = True
|
self.bound = True
|
||||||
log.info("Node set to: %s" % self.boundjid.fulljid)
|
log.info("Node set to: %s" % self.boundjid.full)
|
||||||
session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
|
session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
|
||||||
if "{%s}session" % session_ns not in self.features or self.bindfail:
|
if "{%s}session" % session_ns not in self.features or self.bindfail:
|
||||||
log.debug("Established Session")
|
log.debug("Established Session")
|
||||||
@@ -401,7 +438,7 @@ class ClientXMPP(BaseXMPP):
|
|||||||
"""
|
"""
|
||||||
if self.authenticated and self.bound:
|
if self.authenticated and self.bound:
|
||||||
iq = self.makeIqSet(xml)
|
iq = self.makeIqSet(xml)
|
||||||
response = iq.send()
|
response = iq.send(now=True)
|
||||||
log.debug("Established Session")
|
log.debug("Established Session")
|
||||||
self.sessionstarted = True
|
self.sessionstarted = True
|
||||||
self.session_started_event.set()
|
self.session_started_event.set()
|
||||||
@@ -428,9 +465,19 @@ class ClientXMPP(BaseXMPP):
|
|||||||
'presence': {},
|
'presence': {},
|
||||||
'in_roster': True}
|
'in_roster': True}
|
||||||
self.roster[jid].update(iq['roster']['items'][jid])
|
self.roster[jid].update(iq['roster']['items'][jid])
|
||||||
|
self.event('roster_received', iq)
|
||||||
|
|
||||||
self.event("roster_update", iq)
|
self.event("roster_update", iq)
|
||||||
if iq['type'] == 'set':
|
if iq['type'] == 'set':
|
||||||
iq.reply()
|
iq.reply()
|
||||||
iq.enable('roster')
|
iq.enable('roster')
|
||||||
iq.send()
|
iq.send()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
ClientXMPP.updateRoster = ClientXMPP.update_roster
|
||||||
|
ClientXMPP.delRosterItem = ClientXMPP.del_roster_item
|
||||||
|
ClientXMPP.getRoster = ClientXMPP.get_roster
|
||||||
|
ClientXMPP.registerFeature = ClientXMPP.register_feature
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class ComponentXMPP(BaseXMPP):
|
|||||||
|
|
||||||
handshake = ET.Element('{jabber:component:accept}handshake')
|
handshake = ET.Element('{jabber:component:accept}handshake')
|
||||||
handshake.text = hashlib.sha1(pre_hash).hexdigest().lower()
|
handshake.text = hashlib.sha1(pre_hash).hexdigest().lower()
|
||||||
self.send_xml(handshake)
|
self.send_xml(handshake, now=True)
|
||||||
|
|
||||||
def _handle_handshake(self, xml):
|
def _handle_handshake(self, xml):
|
||||||
"""
|
"""
|
||||||
@@ -138,4 +138,5 @@ class ComponentXMPP(BaseXMPP):
|
|||||||
Arguments:
|
Arguments:
|
||||||
xml -- The reply handshake stanza.
|
xml -- The reply handshake stanza.
|
||||||
"""
|
"""
|
||||||
|
self.session_started_event.set()
|
||||||
self.event("session_start")
|
self.event("session_start")
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ class XMPPError(Exception):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, condition='undefined-condition', text=None, etype=None,
|
def __init__(self, condition='undefined-condition', text=None, etype=None,
|
||||||
extension=None, extension_ns=None, extension_args=None):
|
extension=None, extension_ns=None, extension_args=None,
|
||||||
|
clear=True):
|
||||||
"""
|
"""
|
||||||
Create a new XMPPError exception.
|
Create a new XMPPError exception.
|
||||||
|
|
||||||
@@ -37,6 +38,9 @@ class XMPPError(Exception):
|
|||||||
extension_args -- Content and attributes for the extension
|
extension_args -- Content and attributes for the extension
|
||||||
element. Same as the additional arguments to
|
element. Same as the additional arguments to
|
||||||
the ET.Element constructor.
|
the ET.Element constructor.
|
||||||
|
clear -- Indicates if the stanza's contents should be
|
||||||
|
removed before replying with an error.
|
||||||
|
Defaults to True.
|
||||||
"""
|
"""
|
||||||
if extension_args is None:
|
if extension_args is None:
|
||||||
extension_args = {}
|
extension_args = {}
|
||||||
@@ -44,6 +48,7 @@ class XMPPError(Exception):
|
|||||||
self.condition = condition
|
self.condition = condition
|
||||||
self.text = text
|
self.text = text
|
||||||
self.etype = etype
|
self.etype = etype
|
||||||
|
self.clear = clear
|
||||||
self.extension = extension
|
self.extension = extension
|
||||||
self.extension_ns = extension_ns
|
self.extension_ns = extension_ns
|
||||||
self.extension_args = extension_args
|
self.extension_args = extension_args
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
|
|
||||||
See the file LICENSE for copying permission.
|
See the file LICENSE for copying permission.
|
||||||
"""
|
"""
|
||||||
__all__ = ['xep_0004', 'xep_0012', 'xep_0030', 'xep_0033', 'xep_0045',
|
__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
|
||||||
'xep_0050', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify',
|
'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086',
|
||||||
'xep_0060', 'xep_0202']
|
'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify']
|
||||||
|
|||||||
@@ -9,7 +9,63 @@
|
|||||||
|
|
||||||
class base_plugin(object):
|
class base_plugin(object):
|
||||||
|
|
||||||
def __init__(self, xmpp, config):
|
"""
|
||||||
|
The base_plugin class serves as a base for user created plugins
|
||||||
|
that provide support for existing or experimental XEPS.
|
||||||
|
|
||||||
|
Each plugin has a dictionary for configuration options, as well
|
||||||
|
as a name and description.
|
||||||
|
|
||||||
|
The lifecycle of a plugin is:
|
||||||
|
1. The plugin is instantiated during registration.
|
||||||
|
2. Once the XML stream begins processing, the method
|
||||||
|
plugin_init() is called (if the plugin is configured
|
||||||
|
as enabled with {'enable': True}).
|
||||||
|
3. After all plugins have been initialized, the
|
||||||
|
method post_init() is called.
|
||||||
|
|
||||||
|
Recommended event handlers:
|
||||||
|
session_start -- Plugins which require the use of the current
|
||||||
|
bound JID SHOULD wait for the session_start
|
||||||
|
event to perform any initialization (or
|
||||||
|
resetting). This is a transitive recommendation,
|
||||||
|
plugins that use other plugins which use the
|
||||||
|
bound JID should also wait for session_start
|
||||||
|
before making such calls.
|
||||||
|
session_end -- If the plugin keeps any per-session state,
|
||||||
|
such as joined MUC rooms, such state SHOULD
|
||||||
|
be cleared when the session_end event is raised.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
xep -- The XEP number the plugin implements, if any.
|
||||||
|
description -- A short description of the plugin, typically
|
||||||
|
the long name of the implemented XEP.
|
||||||
|
xmpp -- The main SleekXMPP instance.
|
||||||
|
config -- A dictionary of custom configuration values.
|
||||||
|
The value 'enable' is special and controls
|
||||||
|
whether or not the plugin is initialized
|
||||||
|
after registration.
|
||||||
|
post_initted -- Executed after all plugins have been initialized
|
||||||
|
to handle any cross-plugin interactions, such as
|
||||||
|
registering service discovery items.
|
||||||
|
enable -- Indicates that the plugin is enabled for use and
|
||||||
|
will be initialized after registration.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
plugin_init -- Initialize the plugin state.
|
||||||
|
post_init -- Handle any cross-plugin concerns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, xmpp, config=None):
|
||||||
|
"""
|
||||||
|
Instantiate a new plugin and store the given configuration.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
xmpp -- The main SleekXMPP instance.
|
||||||
|
config -- A dictionary of configuration values.
|
||||||
|
"""
|
||||||
|
if config is None:
|
||||||
|
config = {}
|
||||||
self.xep = 'base'
|
self.xep = 'base'
|
||||||
self.description = 'Base Plugin'
|
self.description = 'Base Plugin'
|
||||||
self.xmpp = xmpp
|
self.xmpp = xmpp
|
||||||
@@ -20,7 +76,15 @@ class base_plugin(object):
|
|||||||
self.plugin_init()
|
self.plugin_init()
|
||||||
|
|
||||||
def plugin_init(self):
|
def plugin_init(self):
|
||||||
|
"""
|
||||||
|
Initialize plugin state, such as registering any stream or
|
||||||
|
event handlers, or new stanza types.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def post_init(self):
|
def post_init(self):
|
||||||
|
"""
|
||||||
|
Perform any cross-plugin interactions, such as registering
|
||||||
|
service discovery identities or items.
|
||||||
|
"""
|
||||||
self.post_inited = True
|
self.post_inited = True
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class gmail_notify(base.base_plugin):
|
|||||||
log.info('Gmail: Searching for emails matching: "%s"' % query)
|
log.info('Gmail: Searching for emails matching: "%s"' % query)
|
||||||
iq = self.xmpp.Iq()
|
iq = self.xmpp.Iq()
|
||||||
iq['type'] = 'get'
|
iq['type'] = 'get'
|
||||||
iq['to'] = self.xmpp.jid
|
iq['to'] = self.xmpp.boundjid.bare
|
||||||
iq['gmail']['q'] = query
|
iq['gmail']['q'] = query
|
||||||
iq['gmail']['newer-than-time'] = newer
|
iq['gmail']['newer-than-time'] = newer
|
||||||
return iq.send()
|
return iq.send()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from . import base
|
from . import base
|
||||||
import logging
|
import logging
|
||||||
from xml.etree import cElementTree as ET
|
from xml.etree import cElementTree as ET
|
||||||
import types
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -43,7 +42,7 @@ class jobs(base.base_plugin):
|
|||||||
iq['psstate']['item'] = jobid
|
iq['psstate']['item'] = jobid
|
||||||
iq['psstate']['payload'] = state
|
iq['psstate']['payload'] = state
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or type(result) == types.BooleanType or result['type'] != 'result':
|
if result is None or type(result) == bool or result['type'] != 'result':
|
||||||
log.error("Unable to change %s:%s to %s" % (node, jobid, state))
|
log.error("Unable to change %s:%s to %s" % (node, jobid, state))
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
See the file LICENSE for copying permission.
|
See the file LICENSE for copying permission.
|
||||||
"""
|
"""
|
||||||
from . import base
|
from . import base
|
||||||
import log
|
import logging
|
||||||
from xml.etree import cElementTree as ET
|
from xml.etree import cElementTree as ET
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import logging
|
|||||||
from xml.etree import cElementTree as ET
|
from xml.etree import cElementTree as ET
|
||||||
import time
|
import time
|
||||||
|
|
||||||
class xep_0050(base.base_plugin):
|
class old_0050(base.base_plugin):
|
||||||
"""
|
"""
|
||||||
XEP-0050 Ad-Hoc Commands
|
XEP-0050 Ad-Hoc Commands
|
||||||
"""
|
"""
|
||||||
@@ -110,7 +110,7 @@ class xep_0050(base.base_plugin):
|
|||||||
if not id:
|
if not id:
|
||||||
id = self.xmpp.getNewId()
|
id = self.xmpp.getNewId()
|
||||||
iq = self.xmpp.makeIqResult(id)
|
iq = self.xmpp.makeIqResult(id)
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
iq.attrib['to'] = to
|
iq.attrib['to'] = to
|
||||||
command = ET.Element('{http://jabber.org/protocol/commands}command')
|
command = ET.Element('{http://jabber.org/protocol/commands}command')
|
||||||
command.attrib['node'] = node
|
command.attrib['node'] = node
|
||||||
@@ -237,6 +237,8 @@ class Unsubscribe(ElementBase):
|
|||||||
def getJid(self):
|
def getJid(self):
|
||||||
return JID(self._getAttr('jid'))
|
return JID(self._getAttr('jid'))
|
||||||
|
|
||||||
|
registerStanzaPlugin(Pubsub, Unsubscribe)
|
||||||
|
|
||||||
class Subscribe(ElementBase):
|
class Subscribe(ElementBase):
|
||||||
namespace = 'http://jabber.org/protocol/pubsub'
|
namespace = 'http://jabber.org/protocol/pubsub'
|
||||||
name = 'subscribe'
|
name = 'subscribe'
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from .. xmlstream.handler.callback import Callback
|
|||||||
from .. xmlstream.matcher.xpath import MatchXPath
|
from .. xmlstream.matcher.xpath import MatchXPath
|
||||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
||||||
from .. stanza.message import Message
|
from .. stanza.message import Message
|
||||||
import types
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -58,6 +57,7 @@ class Form(ElementBase):
|
|||||||
return field
|
return field
|
||||||
|
|
||||||
def getXML(self, type='submit'):
|
def getXML(self, type='submit'):
|
||||||
|
self['type'] = type
|
||||||
log.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py")
|
log.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py")
|
||||||
return self.xml
|
return self.xml
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ class Form(ElementBase):
|
|||||||
|
|
||||||
def merge(self, other):
|
def merge(self, other):
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
if type(other) == types.DictType:
|
if type(other) == dict:
|
||||||
new.setValues(other)
|
new.setValues(other)
|
||||||
return new
|
return new
|
||||||
nfields = new.getFields(use_dict=True)
|
nfields = new.getFields(use_dict=True)
|
||||||
|
|||||||
11
sleekxmpp/plugins/xep_0009/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0009/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0009 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0009.rpc import xep_0009
|
||||||
|
from sleekxmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse
|
||||||
166
sleekxmpp/plugins/xep_0009/binding.py
Normal file
166
sleekxmpp/plugins/xep_0009/binding.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_namespace = 'jabber:iq:rpc'
|
||||||
|
|
||||||
|
def fault2xml(fault):
|
||||||
|
value = dict()
|
||||||
|
value['faultCode'] = fault['code']
|
||||||
|
value['faultString'] = fault['string']
|
||||||
|
fault = ET.Element("fault", {'xmlns': _namespace})
|
||||||
|
fault.append(_py2xml((value)))
|
||||||
|
return fault
|
||||||
|
|
||||||
|
def xml2fault(params):
|
||||||
|
vals = []
|
||||||
|
for value in params.findall('{%s}value' % _namespace):
|
||||||
|
vals.append(_xml2py(value))
|
||||||
|
fault = dict()
|
||||||
|
fault['code'] = vals[0]['faultCode']
|
||||||
|
fault['string'] = vals[0]['faultString']
|
||||||
|
return fault
|
||||||
|
|
||||||
|
def py2xml(*args):
|
||||||
|
params = ET.Element("{%s}params" % _namespace)
|
||||||
|
for x in args:
|
||||||
|
param = ET.Element("{%s}param" % _namespace)
|
||||||
|
param.append(_py2xml(x))
|
||||||
|
params.append(param) #<params><param>...
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _py2xml(*args):
|
||||||
|
for x in args:
|
||||||
|
val = ET.Element("value")
|
||||||
|
if x is None:
|
||||||
|
nil = ET.Element("nil")
|
||||||
|
val.append(nil)
|
||||||
|
elif type(x) is int:
|
||||||
|
i4 = ET.Element("i4")
|
||||||
|
i4.text = str(x)
|
||||||
|
val.append(i4)
|
||||||
|
elif type(x) is bool:
|
||||||
|
boolean = ET.Element("boolean")
|
||||||
|
boolean.text = str(int(x))
|
||||||
|
val.append(boolean)
|
||||||
|
elif type(x) is str:
|
||||||
|
string = ET.Element("string")
|
||||||
|
string.text = x
|
||||||
|
val.append(string)
|
||||||
|
elif type(x) is float:
|
||||||
|
double = ET.Element("double")
|
||||||
|
double.text = str(x)
|
||||||
|
val.append(double)
|
||||||
|
elif type(x) is rpcbase64:
|
||||||
|
b64 = ET.Element("Base64")
|
||||||
|
b64.text = x.encoded()
|
||||||
|
val.append(b64)
|
||||||
|
elif type(x) is rpctime:
|
||||||
|
iso = ET.Element("dateTime.iso8601")
|
||||||
|
iso.text = str(x)
|
||||||
|
val.append(iso)
|
||||||
|
elif type(x) in (list, tuple):
|
||||||
|
array = ET.Element("array")
|
||||||
|
data = ET.Element("data")
|
||||||
|
for y in x:
|
||||||
|
data.append(_py2xml(y))
|
||||||
|
array.append(data)
|
||||||
|
val.append(array)
|
||||||
|
elif type(x) is dict:
|
||||||
|
struct = ET.Element("struct")
|
||||||
|
for y in x.keys():
|
||||||
|
member = ET.Element("member")
|
||||||
|
name = ET.Element("name")
|
||||||
|
name.text = y
|
||||||
|
member.append(name)
|
||||||
|
member.append(_py2xml(x[y]))
|
||||||
|
struct.append(member)
|
||||||
|
val.append(struct)
|
||||||
|
return val
|
||||||
|
|
||||||
|
def xml2py(params):
|
||||||
|
namespace = 'jabber:iq:rpc'
|
||||||
|
vals = []
|
||||||
|
for param in params.findall('{%s}param' % namespace):
|
||||||
|
vals.append(_xml2py(param.find('{%s}value' % namespace)))
|
||||||
|
return vals
|
||||||
|
|
||||||
|
def _xml2py(value):
|
||||||
|
namespace = 'jabber:iq:rpc'
|
||||||
|
if value.find('{%s}nil' % namespace) is not None:
|
||||||
|
return None
|
||||||
|
if value.find('{%s}i4' % namespace) is not None:
|
||||||
|
return int(value.find('{%s}i4' % namespace).text)
|
||||||
|
if value.find('{%s}int' % namespace) is not None:
|
||||||
|
return int(value.find('{%s}int' % namespace).text)
|
||||||
|
if value.find('{%s}boolean' % namespace) is not None:
|
||||||
|
return bool(value.find('{%s}boolean' % namespace).text)
|
||||||
|
if value.find('{%s}string' % namespace) is not None:
|
||||||
|
return value.find('{%s}string' % namespace).text
|
||||||
|
if value.find('{%s}double' % namespace) is not None:
|
||||||
|
return float(value.find('{%s}double' % namespace).text)
|
||||||
|
if value.find('{%s}Base64') is not None:
|
||||||
|
return rpcbase64(value.find('Base64' % namespace).text)
|
||||||
|
if value.find('{%s}dateTime.iso8601') is not None:
|
||||||
|
return rpctime(value.find('{%s}dateTime.iso8601'))
|
||||||
|
if value.find('{%s}struct' % namespace) is not None:
|
||||||
|
struct = {}
|
||||||
|
for member in value.find('{%s}struct' % namespace).findall('{%s}member' % namespace):
|
||||||
|
struct[member.find('{%s}name' % namespace).text] = _xml2py(member.find('{%s}value' % namespace))
|
||||||
|
return struct
|
||||||
|
if value.find('{%s}array' % namespace) is not None:
|
||||||
|
array = []
|
||||||
|
for val in value.find('{%s}array' % namespace).find('{%s}data' % namespace).findall('{%s}value' % namespace):
|
||||||
|
array.append(_xml2py(val))
|
||||||
|
return array
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class rpcbase64(object):
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
#base 64 encoded string
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def decode(self):
|
||||||
|
return base64.decodestring(self.data)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.decode()
|
||||||
|
|
||||||
|
def encoded(self):
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class rpctime(object):
|
||||||
|
|
||||||
|
def __init__(self,data=None):
|
||||||
|
#assume string data is in iso format YYYYMMDDTHH:MM:SS
|
||||||
|
if type(data) is str:
|
||||||
|
self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
|
||||||
|
elif type(data) is time.struct_time:
|
||||||
|
self.timestamp = data
|
||||||
|
elif data is None:
|
||||||
|
self.timestamp = time.gmtime()
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
def iso8601(self):
|
||||||
|
#return a iso8601 string
|
||||||
|
return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.iso8601()
|
||||||
739
sleekxmpp/plugins/xep_0009/remote.py
Normal file
739
sleekxmpp/plugins/xep_0009/remote.py
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from binding import py2xml, xml2py, xml2fault, fault2xml
|
||||||
|
from threading import RLock
|
||||||
|
import abc
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
import sleekxmpp
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def _intercept(method, name, public):
|
||||||
|
def _resolver(instance, *args, **kwargs):
|
||||||
|
log.debug("Locally calling %s.%s with arguments %s." % (instance.FQN(), method.__name__, args))
|
||||||
|
try:
|
||||||
|
value = method(instance, *args, **kwargs)
|
||||||
|
if value == NotImplemented:
|
||||||
|
raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__))
|
||||||
|
return value
|
||||||
|
except InvocationException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise InvocationException("A problem occured calling %s.%s!" % (instance.FQN(), method.__name__), e)
|
||||||
|
_resolver._rpc = public
|
||||||
|
_resolver._rpc_name = method.__name__ if name is None else name
|
||||||
|
return _resolver
|
||||||
|
|
||||||
|
def remote(function_argument, public = True):
|
||||||
|
'''
|
||||||
|
Decorator for methods which are remotely callable. This decorator
|
||||||
|
works in conjunction with classes which extend ABC Endpoint.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
@remote
|
||||||
|
def remote_method(arg1, arg2)
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
function_argument -- a stand-in for either the actual method
|
||||||
|
OR a new name (string) for the method. In that case the
|
||||||
|
method is considered mapped:
|
||||||
|
Example:
|
||||||
|
|
||||||
|
@remote("new_name")
|
||||||
|
def remote_method(arg1, arg2)
|
||||||
|
|
||||||
|
public -- A flag which indicates if this method should be part
|
||||||
|
of the known dictionary of remote methods. Defaults to True.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
@remote(False)
|
||||||
|
def remote_method(arg1, arg2)
|
||||||
|
|
||||||
|
Note: renaming and revising (public vs. private) can be combined.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
@remote("new_name", False)
|
||||||
|
def remote_method(arg1, arg2)
|
||||||
|
'''
|
||||||
|
if hasattr(function_argument, '__call__'):
|
||||||
|
return _intercept(function_argument, None, public)
|
||||||
|
else:
|
||||||
|
if not isinstance(function_argument, basestring):
|
||||||
|
if not isinstance(function_argument, bool):
|
||||||
|
raise Exception('Expected an RPC method name or visibility modifier!')
|
||||||
|
else:
|
||||||
|
def _wrap_revised(function):
|
||||||
|
function = _intercept(function, None, function_argument)
|
||||||
|
return function
|
||||||
|
return _wrap_revised
|
||||||
|
def _wrap_remapped(function):
|
||||||
|
function = _intercept(function, function_argument, public)
|
||||||
|
return function
|
||||||
|
return _wrap_remapped
|
||||||
|
|
||||||
|
|
||||||
|
class ACL:
|
||||||
|
'''
|
||||||
|
An Access Control List (ACL) is a list of rules, which are evaluated
|
||||||
|
in order until a match is found. The policy of the matching rule
|
||||||
|
is then applied.
|
||||||
|
|
||||||
|
Rules are 3-tuples, consisting of a policy enumerated type, a JID
|
||||||
|
expression and a RCP resource expression.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
[ (ACL.ALLOW, '*', '*') ] allow everyone everything, no restrictions
|
||||||
|
[ (ACL.DENY, '*', '*') ] deny everyone everything, no restrictions
|
||||||
|
[ (ACL.ALLOW, 'test@xmpp.org/unit', 'test.*'),
|
||||||
|
(ACL.DENY, '*', '*') ] deny everyone everything, except named
|
||||||
|
JID, which is allowed access to endpoint 'test' only.
|
||||||
|
|
||||||
|
The use of wildcards is allowed in expressions, as follows:
|
||||||
|
'*' everyone, or everything (= all endpoints and methods)
|
||||||
|
'test@xmpp.org/*' every JID regardless of JID resource
|
||||||
|
'*@xmpp.org/rpc' every JID from domain xmpp.org with JID res 'rpc'
|
||||||
|
'frank@*' every 'frank', regardless of domain or JID res
|
||||||
|
'system.*' all methods of endpoint 'system'
|
||||||
|
'*.reboot' all methods reboot regardless of endpoint
|
||||||
|
'''
|
||||||
|
ALLOW = True
|
||||||
|
DENY = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check(cls, rules, jid, resource):
|
||||||
|
if rules is None:
|
||||||
|
return cls.DENY # No rules means no access!
|
||||||
|
for rule in rules:
|
||||||
|
policy = cls._check(rule, jid, resource)
|
||||||
|
if policy is not None:
|
||||||
|
return policy
|
||||||
|
return cls.DENY # By default if not rule matches, deny access.
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check(cls, rule, jid, resource):
|
||||||
|
if cls._match(jid, rule[1]) and cls._match(resource, rule[2]):
|
||||||
|
return rule[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _next_token(cls, expression, index):
|
||||||
|
new_index = expression.find('*', index)
|
||||||
|
if new_index == 0:
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
if new_index == -1:
|
||||||
|
return expression[index : ]
|
||||||
|
else:
|
||||||
|
return expression[index : new_index]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _match(cls, value, expression):
|
||||||
|
#! print "_match [VALUE] %s [EXPR] %s" % (value, expression)
|
||||||
|
index = 0
|
||||||
|
position = 0
|
||||||
|
while index < len(expression):
|
||||||
|
token = cls._next_token(expression, index)
|
||||||
|
#! print "[TOKEN] '%s'" % token
|
||||||
|
size = len(token)
|
||||||
|
if size > 0:
|
||||||
|
token_index = value.find(token, position)
|
||||||
|
if token_index == -1:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
#! print "[INDEX-OF] %s" % token_index
|
||||||
|
position = token_index + len(token)
|
||||||
|
pass
|
||||||
|
if size == 0:
|
||||||
|
index += 1
|
||||||
|
else:
|
||||||
|
index += size
|
||||||
|
#! print "index %s position %s" % (index, position)
|
||||||
|
return True
|
||||||
|
|
||||||
|
ANY_ALL = [ (ACL.ALLOW, '*', '*') ]
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteException(Exception):
|
||||||
|
'''
|
||||||
|
Base exception for RPC. This exception is raised when a problem
|
||||||
|
occurs in the network layer.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, message="", cause=None):
|
||||||
|
'''
|
||||||
|
Initializes a new RemoteException.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
message -- The message accompanying this exception.
|
||||||
|
cause -- The underlying cause of this exception.
|
||||||
|
'''
|
||||||
|
self._message = message
|
||||||
|
self._cause = cause
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self._message)
|
||||||
|
|
||||||
|
def get_message(self):
|
||||||
|
return self._message
|
||||||
|
|
||||||
|
def get_cause(self):
|
||||||
|
return self._cause
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InvocationException(RemoteException):
|
||||||
|
'''
|
||||||
|
Exception raised when a problem occurs during the remote invocation
|
||||||
|
of a method.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationException(RemoteException):
|
||||||
|
'''
|
||||||
|
Exception raised when the caller is not authorized to invoke the
|
||||||
|
remote method.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TimeoutException(Exception):
|
||||||
|
'''
|
||||||
|
Exception raised when the synchronous execution of a method takes
|
||||||
|
longer than the given threshold because an underlying asynchronous
|
||||||
|
reply did not arrive in time.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Callback(object):
|
||||||
|
'''
|
||||||
|
A base class for callback handlers.
|
||||||
|
'''
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def set_value(self, value):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def cancel_with_error(self, exception):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class Future(Callback):
|
||||||
|
'''
|
||||||
|
Represents the result of an asynchronous computation.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
'''
|
||||||
|
Initializes a new Future.
|
||||||
|
'''
|
||||||
|
self._value = None
|
||||||
|
self._exception = None
|
||||||
|
self._event = threading.Event()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
'''
|
||||||
|
Sets the value of this Future. Once the value is set, a caller
|
||||||
|
blocked on get_value will be able to continue.
|
||||||
|
'''
|
||||||
|
self._value = value
|
||||||
|
self._event.set()
|
||||||
|
|
||||||
|
def get_value(self, timeout=None):
|
||||||
|
'''
|
||||||
|
Gets the value of this Future. This call will block until
|
||||||
|
the result is available, or until an optional timeout expires.
|
||||||
|
When this Future is cancelled with an error,
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
timeout -- The maximum waiting time to obtain the value.
|
||||||
|
'''
|
||||||
|
self._event.wait(timeout)
|
||||||
|
if self._exception:
|
||||||
|
raise self._exception
|
||||||
|
if not self._event.is_set():
|
||||||
|
raise TimeoutException
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def is_done(self):
|
||||||
|
'''
|
||||||
|
Returns true if a value has been returned.
|
||||||
|
'''
|
||||||
|
return self._event.is_set()
|
||||||
|
|
||||||
|
def cancel_with_error(self, exception):
|
||||||
|
'''
|
||||||
|
Cancels the Future because of an error. Once cancelled, a
|
||||||
|
caller blocked on get_value will be able to continue.
|
||||||
|
'''
|
||||||
|
self._exception = exception
|
||||||
|
self._event.set()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Endpoint(object):
|
||||||
|
'''
|
||||||
|
The Endpoint class is an abstract base class for all objects
|
||||||
|
participating in an RPC-enabled XMPP network.
|
||||||
|
|
||||||
|
A user subclassing this class is required to implement the method:
|
||||||
|
FQN(self)
|
||||||
|
where FQN stands for Fully Qualified Name, an unambiguous name
|
||||||
|
which specifies which object an RPC call refers to. It is the
|
||||||
|
first part in a RPC method name '<fqn>.<method>'.
|
||||||
|
'''
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, session, target_jid):
|
||||||
|
'''
|
||||||
|
Initialize a new Endpoint. This constructor should never be
|
||||||
|
invoked by a user, instead it will be called by the factories
|
||||||
|
which instantiate the RPC-enabled objects, of which only
|
||||||
|
the classes are provided by the user.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
session -- An RPC session instance.
|
||||||
|
target_jid -- the identity of the remote XMPP entity.
|
||||||
|
'''
|
||||||
|
self.session = session
|
||||||
|
self.target_jid = target_jid
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def FQN(self):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def get_methods(self):
|
||||||
|
'''
|
||||||
|
Returns a dictionary of all RPC method names provided by this
|
||||||
|
class. This method returns the actual method names as found
|
||||||
|
in the class definition which have been decorated with:
|
||||||
|
|
||||||
|
@remote
|
||||||
|
def some_rpc_method(arg1, arg2)
|
||||||
|
|
||||||
|
|
||||||
|
Unless:
|
||||||
|
(1) the name has been remapped, in which case the new
|
||||||
|
name will be returned.
|
||||||
|
|
||||||
|
@remote("new_name")
|
||||||
|
def some_rpc_method(arg1, arg2)
|
||||||
|
|
||||||
|
(2) the method is set to hidden
|
||||||
|
|
||||||
|
@remote(False)
|
||||||
|
def some_hidden_method(arg1, arg2)
|
||||||
|
'''
|
||||||
|
result = dict()
|
||||||
|
for function in dir(self):
|
||||||
|
test_attr = getattr(self, function, None)
|
||||||
|
try:
|
||||||
|
if test_attr._rpc:
|
||||||
|
result[test_attr._rpc_name] = test_attr
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(Endpoint):
|
||||||
|
'''
|
||||||
|
Implementation of the Proxy pattern which is intended to wrap
|
||||||
|
around Endpoints in order to intercept calls, marshall them and
|
||||||
|
forward them to the remote object.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, endpoint, callback = None):
|
||||||
|
'''
|
||||||
|
Initializes a new Proxy.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
endpoint -- The endpoint which is proxified.
|
||||||
|
'''
|
||||||
|
self._endpoint = endpoint
|
||||||
|
self._callback = callback
|
||||||
|
|
||||||
|
def __getattribute__(self, name, *args):
|
||||||
|
if name in ('__dict__', '_endpoint', 'async', '_callback'):
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
else:
|
||||||
|
attribute = self._endpoint.__getattribute__(name)
|
||||||
|
if hasattr(attribute, '__call__'):
|
||||||
|
try:
|
||||||
|
if attribute._rpc:
|
||||||
|
def _remote_call(*args, **kwargs):
|
||||||
|
log.debug("Remotely calling '%s.%s' with arguments %s." % (self._endpoint.FQN(), attribute._rpc_name, args))
|
||||||
|
return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs)
|
||||||
|
return _remote_call
|
||||||
|
except:
|
||||||
|
pass # If the attribute doesn't exist, don't care!
|
||||||
|
return attribute
|
||||||
|
|
||||||
|
def async(self, callback):
|
||||||
|
return Proxy(self._endpoint, callback)
|
||||||
|
|
||||||
|
def get_endpoint(self):
|
||||||
|
'''
|
||||||
|
Returns the proxified endpoint.
|
||||||
|
'''
|
||||||
|
return self._endpoint
|
||||||
|
|
||||||
|
def FQN(self):
|
||||||
|
return self._endpoint.FQN()
|
||||||
|
|
||||||
|
|
||||||
|
class JabberRPCEntry(object):
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, endpoint_FQN, call):
|
||||||
|
self._endpoint_FQN = endpoint_FQN
|
||||||
|
self._call = call
|
||||||
|
|
||||||
|
def call_method(self, args):
|
||||||
|
return_value = self._call(*args)
|
||||||
|
if return_value is None:
|
||||||
|
return return_value
|
||||||
|
else:
|
||||||
|
return self._return(return_value)
|
||||||
|
|
||||||
|
def get_endpoint_FQN(self):
|
||||||
|
return self._endpoint_FQN
|
||||||
|
|
||||||
|
def _return(self, *args):
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteSession(object):
|
||||||
|
'''
|
||||||
|
A context object for a Jabber-RPC session.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, client, session_close_callback):
|
||||||
|
'''
|
||||||
|
Initializes a new RPC session.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
client -- The SleekXMPP client associated with this session.
|
||||||
|
session_close_callback -- A callback called when the
|
||||||
|
session is closed.
|
||||||
|
'''
|
||||||
|
self._client = client
|
||||||
|
self._session_close_callback = session_close_callback
|
||||||
|
self._event = threading.Event()
|
||||||
|
self._entries = {}
|
||||||
|
self._callbacks = {}
|
||||||
|
self._acls = {}
|
||||||
|
self._lock = RLock()
|
||||||
|
|
||||||
|
def _wait(self):
|
||||||
|
self._event.wait()
|
||||||
|
|
||||||
|
def _notify(self, event):
|
||||||
|
log.debug("RPC Session as %s started." % self._client.boundjid.full)
|
||||||
|
self._client.sendPresence()
|
||||||
|
self._event.set()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _register_call(self, endpoint, method, name=None):
|
||||||
|
'''
|
||||||
|
Registers a method from an endpoint as remotely callable.
|
||||||
|
'''
|
||||||
|
if name is None:
|
||||||
|
name = method.__name__
|
||||||
|
key = "%s.%s" % (endpoint, name)
|
||||||
|
log.debug("Registering call handler for %s (%s)." % (key, method))
|
||||||
|
with self._lock:
|
||||||
|
if self._entries.has_key(key):
|
||||||
|
raise KeyError("A handler for %s has already been regisered!" % endpoint)
|
||||||
|
self._entries[key] = JabberRPCEntry(endpoint, method)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def _register_acl(self, endpoint, acl):
|
||||||
|
log.debug("Registering ACL %s for endpoint %s." % (repr(acl), endpoint))
|
||||||
|
with self._lock:
|
||||||
|
self._acls[endpoint] = acl
|
||||||
|
|
||||||
|
def _register_callback(self, pid, callback):
|
||||||
|
with self._lock:
|
||||||
|
self._callbacks[pid] = callback
|
||||||
|
|
||||||
|
def forget_callback(self, callback):
|
||||||
|
with self._lock:
|
||||||
|
pid = self._find_key(self._callbacks, callback)
|
||||||
|
if pid is not None:
|
||||||
|
del self._callback[pid]
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown callback!")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _find_key(self, dict, value):
|
||||||
|
"""return the key of dictionary dic given the value"""
|
||||||
|
search = [k for k, v in dict.iteritems() if v == value]
|
||||||
|
if len(search) == 0:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return search[0]
|
||||||
|
|
||||||
|
def _unregister_call(self, key):
|
||||||
|
#removes the registered call
|
||||||
|
with self._lock:
|
||||||
|
if self._entries[key]:
|
||||||
|
del self._entries[key]
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
def new_proxy(self, target_jid, endpoint_cls):
|
||||||
|
'''
|
||||||
|
Instantiates a new proxy object, which proxies to a remote
|
||||||
|
endpoint. This method uses a class reference without
|
||||||
|
constructor arguments to instantiate the proxy.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
target_jid -- the XMPP entity ID hosting the endpoint.
|
||||||
|
endpoint_cls -- The remote (duck) type.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
argspec = inspect.getargspec(endpoint_cls.__init__)
|
||||||
|
args = [None] * (len(argspec[0]) - 1)
|
||||||
|
result = endpoint_cls(*args)
|
||||||
|
Endpoint.__init__(result, self, target_jid)
|
||||||
|
return Proxy(result)
|
||||||
|
except:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
|
||||||
|
def new_handler(self, acl, handler_cls, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
Instantiates a new handler object, which is called remotely
|
||||||
|
by others. The user can control the effect of the call by
|
||||||
|
implementing the remote method in the local endpoint class. The
|
||||||
|
returned reference can be called locally and will behave as a
|
||||||
|
regular instance.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
acl -- Access control list (see ACL class)
|
||||||
|
handler_clss -- The local (duck) type.
|
||||||
|
*args -- Constructor arguments for the local type.
|
||||||
|
**kwargs -- Constructor keyworded arguments for the local
|
||||||
|
type.
|
||||||
|
'''
|
||||||
|
argspec = inspect.getargspec(handler_cls.__init__)
|
||||||
|
base_argspec = inspect.getargspec(Endpoint.__init__)
|
||||||
|
if(argspec == base_argspec):
|
||||||
|
result = handler_cls(self, self._client.boundjid.full)
|
||||||
|
else:
|
||||||
|
result = handler_cls(*args, **kwargs)
|
||||||
|
Endpoint.__init__(result, self, self._client.boundjid.full)
|
||||||
|
method_dict = result.get_methods()
|
||||||
|
for method_name, method in method_dict.iteritems():
|
||||||
|
#!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name)
|
||||||
|
self._register_call(result.FQN(), method, method_name)
|
||||||
|
self._register_acl(result.FQN(), acl)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# def is_available(self, targetCls, pto):
|
||||||
|
# return self._client.is_available(pto)
|
||||||
|
|
||||||
|
def _call_remote(self, pto, pmethod, callback, *arguments):
|
||||||
|
iq = self._client.plugin['xep_0009'].make_iq_method_call(pto, pmethod, py2xml(*arguments))
|
||||||
|
pid = iq['id']
|
||||||
|
if callback is None:
|
||||||
|
future = Future()
|
||||||
|
self._register_callback(pid, future)
|
||||||
|
iq.send()
|
||||||
|
return future.get_value(30)
|
||||||
|
else:
|
||||||
|
log.debug("[RemoteSession] _call_remote %s" % callback)
|
||||||
|
self._register_callback(pid, callback)
|
||||||
|
iq.send()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
'''
|
||||||
|
Closes this session.
|
||||||
|
'''
|
||||||
|
self._client.disconnect(False)
|
||||||
|
self._session_close_callback()
|
||||||
|
|
||||||
|
def _on_jabber_rpc_method_call(self, iq):
|
||||||
|
iq.enable('rpc_query')
|
||||||
|
params = iq['rpc_query']['method_call']['params']
|
||||||
|
args = xml2py(params)
|
||||||
|
pmethod = iq['rpc_query']['method_call']['method_name']
|
||||||
|
try:
|
||||||
|
with self._lock:
|
||||||
|
entry = self._entries[pmethod]
|
||||||
|
rules = self._acls[entry.get_endpoint_FQN()]
|
||||||
|
if ACL.check(rules, iq['from'], pmethod):
|
||||||
|
return_value = entry.call_method(args)
|
||||||
|
else:
|
||||||
|
raise AuthorizationException("Unauthorized access to %s from %s!" % (pmethod, iq['from']))
|
||||||
|
if return_value is None:
|
||||||
|
return_value = ()
|
||||||
|
response = self._client.plugin['xep_0009'].make_iq_method_response(iq['id'], iq['from'], py2xml(*return_value))
|
||||||
|
response.send()
|
||||||
|
except InvocationException as ie:
|
||||||
|
fault = dict()
|
||||||
|
fault['code'] = 500
|
||||||
|
fault['string'] = ie.get_message()
|
||||||
|
self._client.plugin['xep_0009']._send_fault(iq, fault2xml(fault))
|
||||||
|
except AuthorizationException as ae:
|
||||||
|
log.error(ae.get_message())
|
||||||
|
error = self._client.plugin['xep_0009']._forbidden(iq)
|
||||||
|
error.send()
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, KeyError):
|
||||||
|
log.error("No handler available for %s!" % pmethod)
|
||||||
|
error = self._client.plugin['xep_0009']._item_not_found(iq)
|
||||||
|
else:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
log.error("An unexpected problem occurred invoking method %s!" % pmethod)
|
||||||
|
error = self._client.plugin['xep_0009']._undefined_condition(iq)
|
||||||
|
#! print "[REMOTE.PY] _handle_remote_procedure_call AN ERROR SHOULD BE SENT NOW %s " % e
|
||||||
|
error.send()
|
||||||
|
|
||||||
|
def _on_jabber_rpc_method_response(self, iq):
|
||||||
|
iq.enable('rpc_query')
|
||||||
|
args = xml2py(iq['rpc_query']['method_response']['params'])
|
||||||
|
pid = iq['id']
|
||||||
|
with self._lock:
|
||||||
|
callback = self._callbacks[pid]
|
||||||
|
del self._callbacks[pid]
|
||||||
|
if(len(args) > 0):
|
||||||
|
callback.set_value(args[0])
|
||||||
|
else:
|
||||||
|
callback.set_value(None)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_jabber_rpc_method_response2(self, iq):
|
||||||
|
iq.enable('rpc_query')
|
||||||
|
if iq['rpc_query']['method_response']['fault'] is not None:
|
||||||
|
self._on_jabber_rpc_method_fault(iq)
|
||||||
|
else:
|
||||||
|
args = xml2py(iq['rpc_query']['method_response']['params'])
|
||||||
|
pid = iq['id']
|
||||||
|
with self._lock:
|
||||||
|
callback = self._callbacks[pid]
|
||||||
|
del self._callbacks[pid]
|
||||||
|
if(len(args) > 0):
|
||||||
|
callback.set_value(args[0])
|
||||||
|
else:
|
||||||
|
callback.set_value(None)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_jabber_rpc_method_fault(self, iq):
|
||||||
|
iq.enable('rpc_query')
|
||||||
|
fault = xml2fault(iq['rpc_query']['method_response']['fault'])
|
||||||
|
pid = iq['id']
|
||||||
|
with self._lock:
|
||||||
|
callback = self._callbacks[pid]
|
||||||
|
del self._callbacks[pid]
|
||||||
|
e = {
|
||||||
|
500: InvocationException
|
||||||
|
}[fault['code']](fault['string'])
|
||||||
|
callback.cancel_with_error(e)
|
||||||
|
|
||||||
|
def _on_jabber_rpc_error(self, iq):
|
||||||
|
pid = iq['id']
|
||||||
|
pmethod = self._client.plugin['xep_0009']._extract_method(iq['rpc_query'])
|
||||||
|
code = iq['error']['code']
|
||||||
|
type = iq['error']['type']
|
||||||
|
condition = iq['error']['condition']
|
||||||
|
#! print("['REMOTE.PY']._BINDING_handle_remote_procedure_error -> ERROR! ERROR! ERROR! Condition is '%s'" % condition)
|
||||||
|
with self._lock:
|
||||||
|
callback = self._callbacks[pid]
|
||||||
|
del self._callbacks[pid]
|
||||||
|
e = {
|
||||||
|
'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])),
|
||||||
|
'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])),
|
||||||
|
'undefined-condition': RemoteException("An unexpected problem occured trying to invoke %s at %s!" % (pmethod, iq['from'])),
|
||||||
|
}[condition]
|
||||||
|
if e is None:
|
||||||
|
RemoteException("An unexpected exception occurred at %s!" % iq['from'])
|
||||||
|
callback.cancel_with_error(e)
|
||||||
|
|
||||||
|
|
||||||
|
class Remote(object):
|
||||||
|
'''
|
||||||
|
Bootstrap class for Jabber-RPC sessions. New sessions are openend
|
||||||
|
with an existing XMPP client, or one is instantiated on demand.
|
||||||
|
'''
|
||||||
|
_instance = None
|
||||||
|
_sessions = dict()
|
||||||
|
_lock = threading.RLock()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_session_with_client(cls, client, callback=None):
|
||||||
|
'''
|
||||||
|
Opens a new session with a given client.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
client -- An XMPP client.
|
||||||
|
callback -- An optional callback which can be used to track
|
||||||
|
the starting state of the session.
|
||||||
|
'''
|
||||||
|
with Remote._lock:
|
||||||
|
if(client.boundjid.bare in cls._sessions):
|
||||||
|
raise RemoteException("There already is a session associated with these credentials!")
|
||||||
|
else:
|
||||||
|
cls._sessions[client.boundjid.bare] = client;
|
||||||
|
def _session_close_callback():
|
||||||
|
with Remote._lock:
|
||||||
|
del cls._sessions[client.boundjid.bare]
|
||||||
|
result = RemoteSession(client, _session_close_callback)
|
||||||
|
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call)
|
||||||
|
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response)
|
||||||
|
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault)
|
||||||
|
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error)
|
||||||
|
if callback is None:
|
||||||
|
start_event_handler = result._notify
|
||||||
|
else:
|
||||||
|
start_event_handler = callback
|
||||||
|
client.add_event_handler("session_start", start_event_handler)
|
||||||
|
if client.connect():
|
||||||
|
client.process(threaded=True)
|
||||||
|
else:
|
||||||
|
raise RemoteException("Could not connect to XMPP server!")
|
||||||
|
pass
|
||||||
|
if callback is None:
|
||||||
|
result._wait()
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_session(cls, jid, password, callback=None):
|
||||||
|
'''
|
||||||
|
Opens a new session and instantiates a new XMPP client.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The XMPP JID for logging in.
|
||||||
|
password -- The password for logging in.
|
||||||
|
callback -- An optional callback which can be used to track
|
||||||
|
the starting state of the session.
|
||||||
|
'''
|
||||||
|
client = sleekxmpp.ClientXMPP(jid, password)
|
||||||
|
#? Register plug-ins.
|
||||||
|
client.registerPlugin('xep_0004') # Data Forms
|
||||||
|
client.registerPlugin('xep_0009') # Jabber-RPC
|
||||||
|
client.registerPlugin('xep_0030') # Service Discovery
|
||||||
|
client.registerPlugin('xep_0060') # PubSub
|
||||||
|
client.registerPlugin('xep_0199') # XMPP Ping
|
||||||
|
return cls.new_session_with_client(client, callback)
|
||||||
|
|
||||||
221
sleekxmpp/plugins/xep_0009/rpc.py
Normal file
221
sleekxmpp/plugins/xep_0009/rpc.py
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins import base
|
||||||
|
from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
|
||||||
|
from sleekxmpp.stanza.iq import Iq
|
||||||
|
from sleekxmpp.xmlstream.handler.callback import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
|
||||||
|
from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0009(base.base_plugin):
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = '0009'
|
||||||
|
self.description = 'Jabber-RPC'
|
||||||
|
#self.stanza = sleekxmpp.plugins.xep_0009.stanza
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, RPCQuery)
|
||||||
|
register_stanza_plugin(RPCQuery, MethodCall)
|
||||||
|
register_stanza_plugin(RPCQuery, MethodResponse)
|
||||||
|
|
||||||
|
self.xmpp.registerHandler(
|
||||||
|
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodCall' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
|
||||||
|
self._handle_method_call)
|
||||||
|
)
|
||||||
|
self.xmpp.registerHandler(
|
||||||
|
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
|
||||||
|
self._handle_method_response)
|
||||||
|
)
|
||||||
|
self.xmpp.registerHandler(
|
||||||
|
Callback('RPC Call', MatchXPath('{%s}iq/{%s}error' % (self.xmpp.default_ns, self.xmpp.default_ns)),
|
||||||
|
self._handle_error)
|
||||||
|
)
|
||||||
|
self.xmpp.add_event_handler('jabber_rpc_method_call', self._on_jabber_rpc_method_call)
|
||||||
|
self.xmpp.add_event_handler('jabber_rpc_method_response', self._on_jabber_rpc_method_response)
|
||||||
|
self.xmpp.add_event_handler('jabber_rpc_method_fault', self._on_jabber_rpc_method_fault)
|
||||||
|
self.xmpp.add_event_handler('jabber_rpc_error', self._on_jabber_rpc_error)
|
||||||
|
self.xmpp.add_event_handler('error', self._handle_error)
|
||||||
|
#self.activeCalls = []
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
base.base_plugin.post_init(self)
|
||||||
|
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
|
||||||
|
self.xmpp.plugin['xep_0030'].add_identity('automation','rpc')
|
||||||
|
|
||||||
|
def make_iq_method_call(self, pto, pmethod, params):
|
||||||
|
iq = self.xmpp.makeIqSet()
|
||||||
|
iq.attrib['to'] = pto
|
||||||
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
|
iq.enable('rpc_query')
|
||||||
|
iq['rpc_query']['method_call']['method_name'] = pmethod
|
||||||
|
iq['rpc_query']['method_call']['params'] = params
|
||||||
|
return iq;
|
||||||
|
|
||||||
|
def make_iq_method_response(self, pid, pto, params):
|
||||||
|
iq = self.xmpp.makeIqResult(pid)
|
||||||
|
iq.attrib['to'] = pto
|
||||||
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
|
iq.enable('rpc_query')
|
||||||
|
iq['rpc_query']['method_response']['params'] = params
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def make_iq_method_response_fault(self, pid, pto, params):
|
||||||
|
iq = self.xmpp.makeIqResult(pid)
|
||||||
|
iq.attrib['to'] = pto
|
||||||
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
|
iq.enable('rpc_query')
|
||||||
|
iq['rpc_query']['method_response']['params'] = None
|
||||||
|
iq['rpc_query']['method_response']['fault'] = params
|
||||||
|
return iq
|
||||||
|
|
||||||
|
# def make_iq_method_error(self, pto, pid, pmethod, params, code, type, condition):
|
||||||
|
# iq = self.xmpp.makeIqError(pid)
|
||||||
|
# iq.attrib['to'] = pto
|
||||||
|
# iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
|
# iq['error']['code'] = code
|
||||||
|
# iq['error']['type'] = type
|
||||||
|
# iq['error']['condition'] = condition
|
||||||
|
# iq['rpc_query']['method_call']['method_name'] = pmethod
|
||||||
|
# iq['rpc_query']['method_call']['params'] = params
|
||||||
|
# return iq
|
||||||
|
|
||||||
|
def _item_not_found(self, iq):
|
||||||
|
payload = iq.get_payload()
|
||||||
|
iq.reply().error().set_payload(payload);
|
||||||
|
iq['error']['code'] = '404'
|
||||||
|
iq['error']['type'] = 'cancel'
|
||||||
|
iq['error']['condition'] = 'item-not-found'
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def _undefined_condition(self, iq):
|
||||||
|
payload = iq.get_payload()
|
||||||
|
iq.reply().error().set_payload(payload)
|
||||||
|
iq['error']['code'] = '500'
|
||||||
|
iq['error']['type'] = 'cancel'
|
||||||
|
iq['error']['condition'] = 'undefined-condition'
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def _forbidden(self, iq):
|
||||||
|
payload = iq.get_payload()
|
||||||
|
iq.reply().error().set_payload(payload)
|
||||||
|
iq['error']['code'] = '403'
|
||||||
|
iq['error']['type'] = 'auth'
|
||||||
|
iq['error']['condition'] = 'forbidden'
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def _recipient_unvailable(self, iq):
|
||||||
|
payload = iq.get_payload()
|
||||||
|
iq.reply().error().set_payload(payload)
|
||||||
|
iq['error']['code'] = '404'
|
||||||
|
iq['error']['type'] = 'wait'
|
||||||
|
iq['error']['condition'] = 'recipient-unavailable'
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def _handle_method_call(self, iq):
|
||||||
|
type = iq['type']
|
||||||
|
if type == 'set':
|
||||||
|
log.debug("Incoming Jabber-RPC call from %s" % iq['from'])
|
||||||
|
self.xmpp.event('jabber_rpc_method_call', iq)
|
||||||
|
else:
|
||||||
|
if type == 'error' and ['rpc_query'] is None:
|
||||||
|
self.handle_error(iq)
|
||||||
|
else:
|
||||||
|
log.debug("Incoming Jabber-RPC error from %s" % iq['from'])
|
||||||
|
self.xmpp.event('jabber_rpc_error', iq)
|
||||||
|
|
||||||
|
def _handle_method_response(self, iq):
|
||||||
|
if iq['rpc_query']['method_response']['fault'] is not None:
|
||||||
|
log.debug("Incoming Jabber-RPC fault from %s" % iq['from'])
|
||||||
|
#self._on_jabber_rpc_method_fault(iq)
|
||||||
|
self.xmpp.event('jabber_rpc_method_fault', iq)
|
||||||
|
else:
|
||||||
|
log.debug("Incoming Jabber-RPC response from %s" % iq['from'])
|
||||||
|
self.xmpp.event('jabber_rpc_method_response', iq)
|
||||||
|
|
||||||
|
def _handle_error(self, iq):
|
||||||
|
print("['XEP-0009']._handle_error -> ERROR! Iq is '%s'" % iq)
|
||||||
|
print("#######################")
|
||||||
|
print("### NOT IMPLEMENTED ###")
|
||||||
|
print("#######################")
|
||||||
|
|
||||||
|
def _on_jabber_rpc_method_call(self, iq, forwarded=False):
|
||||||
|
"""
|
||||||
|
A default handler for Jabber-RPC method call. If another
|
||||||
|
handler is registered, this one will defer and not run.
|
||||||
|
|
||||||
|
If this handler is called by your own custom handler with
|
||||||
|
forwarded set to True, then it will run as normal.
|
||||||
|
"""
|
||||||
|
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1:
|
||||||
|
return
|
||||||
|
# Reply with error by default
|
||||||
|
error = self.client.plugin['xep_0009']._item_not_found(iq)
|
||||||
|
error.send()
|
||||||
|
|
||||||
|
def _on_jabber_rpc_method_response(self, iq, forwarded=False):
|
||||||
|
"""
|
||||||
|
A default handler for Jabber-RPC method response. If another
|
||||||
|
handler is registered, this one will defer and not run.
|
||||||
|
|
||||||
|
If this handler is called by your own custom handler with
|
||||||
|
forwarded set to True, then it will run as normal.
|
||||||
|
"""
|
||||||
|
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1:
|
||||||
|
return
|
||||||
|
error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
|
||||||
|
error.send()
|
||||||
|
|
||||||
|
def _on_jabber_rpc_method_fault(self, iq, forwarded=False):
|
||||||
|
"""
|
||||||
|
A default handler for Jabber-RPC fault response. If another
|
||||||
|
handler is registered, this one will defer and not run.
|
||||||
|
|
||||||
|
If this handler is called by your own custom handler with
|
||||||
|
forwarded set to True, then it will run as normal.
|
||||||
|
"""
|
||||||
|
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1:
|
||||||
|
return
|
||||||
|
error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
|
||||||
|
error.send()
|
||||||
|
|
||||||
|
def _on_jabber_rpc_error(self, iq, forwarded=False):
|
||||||
|
"""
|
||||||
|
A default handler for Jabber-RPC error response. If another
|
||||||
|
handler is registered, this one will defer and not run.
|
||||||
|
|
||||||
|
If this handler is called by your own custom handler with
|
||||||
|
forwarded set to True, then it will run as normal.
|
||||||
|
"""
|
||||||
|
if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1:
|
||||||
|
return
|
||||||
|
error = self.client.plugin['xep_0009']._recpient_unavailable(iq, iq.get_payload())
|
||||||
|
error.send()
|
||||||
|
|
||||||
|
def _send_fault(self, iq, fault_xml): #
|
||||||
|
fault = self.make_iq_method_response_fault(iq['id'], iq['from'], fault_xml)
|
||||||
|
fault.send()
|
||||||
|
|
||||||
|
def _send_error(self, iq):
|
||||||
|
print("['XEP-0009']._send_error -> ERROR! Iq is '%s'" % iq)
|
||||||
|
print("#######################")
|
||||||
|
print("### NOT IMPLEMENTED ###")
|
||||||
|
print("#######################")
|
||||||
|
|
||||||
|
def _extract_method(self, stanza):
|
||||||
|
xml = ET.fromstring("%s" % stanza)
|
||||||
|
return xml.find("./methodCall/methodName").text
|
||||||
|
|
||||||
64
sleekxmpp/plugins/xep_0009/stanza/RPC.py
Normal file
64
sleekxmpp/plugins/xep_0009/stanza/RPC.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream.stanzabase import ElementBase
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
class RPCQuery(ElementBase):
|
||||||
|
name = 'query'
|
||||||
|
namespace = 'jabber:iq:rpc'
|
||||||
|
plugin_attrib = 'rpc_query'
|
||||||
|
interfaces = set(())
|
||||||
|
subinterfaces = set(())
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
|
||||||
|
class MethodCall(ElementBase):
|
||||||
|
name = 'methodCall'
|
||||||
|
namespace = 'jabber:iq:rpc'
|
||||||
|
plugin_attrib = 'method_call'
|
||||||
|
interfaces = set(('method_name', 'params'))
|
||||||
|
subinterfaces = set(())
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def get_method_name(self):
|
||||||
|
return self._get_sub_text('methodName')
|
||||||
|
|
||||||
|
def set_method_name(self, value):
|
||||||
|
return self._set_sub_text('methodName', value)
|
||||||
|
|
||||||
|
def get_params(self):
|
||||||
|
return self.xml.find('{%s}params' % self.namespace)
|
||||||
|
|
||||||
|
def set_params(self, params):
|
||||||
|
self.append(params)
|
||||||
|
|
||||||
|
|
||||||
|
class MethodResponse(ElementBase):
|
||||||
|
name = 'methodResponse'
|
||||||
|
namespace = 'jabber:iq:rpc'
|
||||||
|
plugin_attrib = 'method_response'
|
||||||
|
interfaces = set(('params', 'fault'))
|
||||||
|
subinterfaces = set(())
|
||||||
|
plugin_attrib_map = {}
|
||||||
|
plugin_tag_map = {}
|
||||||
|
|
||||||
|
def get_params(self):
|
||||||
|
return self.xml.find('{%s}params' % self.namespace)
|
||||||
|
|
||||||
|
def set_params(self, params):
|
||||||
|
self.append(params)
|
||||||
|
|
||||||
|
def get_fault(self):
|
||||||
|
return self.xml.find('{%s}fault' % self.namespace)
|
||||||
|
|
||||||
|
def set_fault(self, fault):
|
||||||
|
self.append(fault)
|
||||||
9
sleekxmpp/plugins/xep_0009/stanza/__init__.py
Normal file
9
sleekxmpp/plugins/xep_0009/stanza/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
"""
|
|
||||||
SleekXMPP: The Sleek XMPP Library
|
|
||||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
|
||||||
This file is part of SleekXMPP.
|
|
||||||
|
|
||||||
See the file LICENSE for copying permission.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from . import base
|
|
||||||
from .. xmlstream.handler.callback import Callback
|
|
||||||
from .. xmlstream.matcher.xpath import MatchXPath
|
|
||||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
|
||||||
from .. stanza.iq import Iq
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoInfo(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/disco#info'
|
|
||||||
name = 'query'
|
|
||||||
plugin_attrib = 'disco_info'
|
|
||||||
interfaces = set(('node', 'features', 'identities'))
|
|
||||||
|
|
||||||
def getFeatures(self):
|
|
||||||
features = []
|
|
||||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
|
||||||
for feature in featuresXML:
|
|
||||||
features.append(feature.attrib['var'])
|
|
||||||
return features
|
|
||||||
|
|
||||||
def setFeatures(self, features):
|
|
||||||
self.delFeatures()
|
|
||||||
for name in features:
|
|
||||||
self.addFeature(name)
|
|
||||||
|
|
||||||
def delFeatures(self):
|
|
||||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
|
||||||
for feature in featuresXML:
|
|
||||||
self.xml.remove(feature)
|
|
||||||
|
|
||||||
def addFeature(self, feature):
|
|
||||||
featureXML = ET.Element('{%s}feature' % self.namespace,
|
|
||||||
{'var': feature})
|
|
||||||
self.xml.append(featureXML)
|
|
||||||
|
|
||||||
def delFeature(self, feature):
|
|
||||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
|
||||||
for featureXML in featuresXML:
|
|
||||||
if featureXML.attrib['var'] == feature:
|
|
||||||
self.xml.remove(featureXML)
|
|
||||||
|
|
||||||
def getIdentities(self):
|
|
||||||
ids = []
|
|
||||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
|
||||||
for idXML in idsXML:
|
|
||||||
idData = (idXML.attrib['category'],
|
|
||||||
idXML.attrib['type'],
|
|
||||||
idXML.attrib.get('name', ''))
|
|
||||||
ids.append(idData)
|
|
||||||
return ids
|
|
||||||
|
|
||||||
def setIdentities(self, ids):
|
|
||||||
self.delIdentities()
|
|
||||||
for idData in ids:
|
|
||||||
self.addIdentity(*idData)
|
|
||||||
|
|
||||||
def delIdentities(self):
|
|
||||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
|
||||||
for idXML in idsXML:
|
|
||||||
self.xml.remove(idXML)
|
|
||||||
|
|
||||||
def addIdentity(self, category, id_type, name=''):
|
|
||||||
idXML = ET.Element('{%s}identity' % self.namespace,
|
|
||||||
{'category': category,
|
|
||||||
'type': id_type,
|
|
||||||
'name': name})
|
|
||||||
self.xml.append(idXML)
|
|
||||||
|
|
||||||
def delIdentity(self, category, id_type, name=''):
|
|
||||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
|
||||||
for idXML in idsXML:
|
|
||||||
idData = (idXML.attrib['category'],
|
|
||||||
idXML.attrib['type'])
|
|
||||||
delId = (category, id_type)
|
|
||||||
if idData == delId:
|
|
||||||
self.xml.remove(idXML)
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoItems(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/disco#items'
|
|
||||||
name = 'query'
|
|
||||||
plugin_attrib = 'disco_items'
|
|
||||||
interfaces = set(('node', 'items'))
|
|
||||||
|
|
||||||
def getItems(self):
|
|
||||||
items = []
|
|
||||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
|
||||||
for item in itemsXML:
|
|
||||||
itemData = (item.attrib['jid'],
|
|
||||||
item.attrib.get('node'),
|
|
||||||
item.attrib.get('name'))
|
|
||||||
items.append(itemData)
|
|
||||||
return items
|
|
||||||
|
|
||||||
def setItems(self, items):
|
|
||||||
self.delItems()
|
|
||||||
for item in items:
|
|
||||||
self.addItem(*item)
|
|
||||||
|
|
||||||
def delItems(self):
|
|
||||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
|
||||||
for item in itemsXML:
|
|
||||||
self.xml.remove(item)
|
|
||||||
|
|
||||||
def addItem(self, jid, node='', name=''):
|
|
||||||
itemXML = ET.Element('{%s}item' % self.namespace, {'jid': jid})
|
|
||||||
if name:
|
|
||||||
itemXML.attrib['name'] = name
|
|
||||||
if node:
|
|
||||||
itemXML.attrib['node'] = node
|
|
||||||
self.xml.append(itemXML)
|
|
||||||
|
|
||||||
def delItem(self, jid, node=''):
|
|
||||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
|
||||||
for itemXML in itemsXML:
|
|
||||||
itemData = (itemXML.attrib['jid'],
|
|
||||||
itemXML.attrib.get('node', ''))
|
|
||||||
itemDel = (jid, node)
|
|
||||||
if itemData == itemDel:
|
|
||||||
self.xml.remove(itemXML)
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoNode(object):
|
|
||||||
"""
|
|
||||||
Collection object for grouping info and item information
|
|
||||||
into nodes.
|
|
||||||
"""
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
self.info = DiscoInfo()
|
|
||||||
self.items = DiscoItems()
|
|
||||||
|
|
||||||
self.info['node'] = name
|
|
||||||
self.items['node'] = name
|
|
||||||
|
|
||||||
# This is a bit like poor man's inheritance, but
|
|
||||||
# to simplify adding information to the node we
|
|
||||||
# map node functions to either the info or items
|
|
||||||
# stanza objects.
|
|
||||||
#
|
|
||||||
# We don't want to make DiscoNode inherit from
|
|
||||||
# DiscoInfo and DiscoItems because DiscoNode is
|
|
||||||
# not an actual stanza, and doing so would create
|
|
||||||
# confusion and potential bugs.
|
|
||||||
|
|
||||||
self._map(self.items, 'items', ['get', 'set', 'del'])
|
|
||||||
self._map(self.items, 'item', ['add', 'del'])
|
|
||||||
self._map(self.info, 'identities', ['get', 'set', 'del'])
|
|
||||||
self._map(self.info, 'identity', ['add', 'del'])
|
|
||||||
self._map(self.info, 'features', ['get', 'set', 'del'])
|
|
||||||
self._map(self.info, 'feature', ['add', 'del'])
|
|
||||||
|
|
||||||
def isEmpty(self):
|
|
||||||
"""
|
|
||||||
Test if the node contains any information. Useful for
|
|
||||||
determining if a node can be deleted.
|
|
||||||
"""
|
|
||||||
ids = self.getIdentities()
|
|
||||||
features = self.getFeatures()
|
|
||||||
items = self.getItems()
|
|
||||||
|
|
||||||
if not ids and not features and not items:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _map(self, obj, interface, access):
|
|
||||||
"""
|
|
||||||
Map functions of the form obj.accessInterface
|
|
||||||
to self.accessInterface for each given access type.
|
|
||||||
"""
|
|
||||||
interface = interface.title()
|
|
||||||
for access_type in access:
|
|
||||||
method = access_type + interface
|
|
||||||
if hasattr(obj, method):
|
|
||||||
setattr(self, method, getattr(obj, method))
|
|
||||||
|
|
||||||
|
|
||||||
class xep_0030(base.base_plugin):
|
|
||||||
"""
|
|
||||||
XEP-0030 Service Discovery
|
|
||||||
"""
|
|
||||||
|
|
||||||
def plugin_init(self):
|
|
||||||
self.xep = '0030'
|
|
||||||
self.description = 'Service Discovery'
|
|
||||||
|
|
||||||
self.xmpp.registerHandler(
|
|
||||||
Callback('Disco Items',
|
|
||||||
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
|
|
||||||
DiscoItems.namespace)),
|
|
||||||
self.handle_item_query))
|
|
||||||
|
|
||||||
self.xmpp.registerHandler(
|
|
||||||
Callback('Disco Info',
|
|
||||||
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
|
|
||||||
DiscoInfo.namespace)),
|
|
||||||
self.handle_info_query))
|
|
||||||
|
|
||||||
registerStanzaPlugin(Iq, DiscoInfo)
|
|
||||||
registerStanzaPlugin(Iq, DiscoItems)
|
|
||||||
|
|
||||||
self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items)
|
|
||||||
self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info)
|
|
||||||
|
|
||||||
self.nodes = {'main': DiscoNode('main')}
|
|
||||||
|
|
||||||
def add_node(self, node):
|
|
||||||
if node not in self.nodes:
|
|
||||||
self.nodes[node] = DiscoNode(node)
|
|
||||||
|
|
||||||
def del_node(self, node):
|
|
||||||
if node in self.nodes:
|
|
||||||
del self.nodes[node]
|
|
||||||
|
|
||||||
def handle_item_query(self, iq):
|
|
||||||
if iq['type'] == 'get':
|
|
||||||
log.debug("Items requested by %s" % iq['from'])
|
|
||||||
self.xmpp.event('disco_items_request', iq)
|
|
||||||
elif iq['type'] == 'result':
|
|
||||||
log.debug("Items result from %s" % iq['from'])
|
|
||||||
self.xmpp.event('disco_items', iq)
|
|
||||||
|
|
||||||
def handle_info_query(self, iq):
|
|
||||||
if iq['type'] == 'get':
|
|
||||||
log.debug("Info requested by %s" % iq['from'])
|
|
||||||
self.xmpp.event('disco_info_request', iq)
|
|
||||||
elif iq['type'] == 'result':
|
|
||||||
log.debug("Info result from %s" % iq['from'])
|
|
||||||
self.xmpp.event('disco_info', iq)
|
|
||||||
|
|
||||||
def handle_disco_info(self, iq, forwarded=False):
|
|
||||||
"""
|
|
||||||
A default handler for disco#info requests. If another
|
|
||||||
handler is registered, this one will defer and not run.
|
|
||||||
"""
|
|
||||||
if not forwarded and self.xmpp.event_handled('disco_info_request'):
|
|
||||||
return
|
|
||||||
|
|
||||||
node_name = iq['disco_info']['node']
|
|
||||||
if not node_name:
|
|
||||||
node_name = 'main'
|
|
||||||
|
|
||||||
log.debug("Using default handler for disco#info on node '%s'." % node_name)
|
|
||||||
|
|
||||||
if node_name in self.nodes:
|
|
||||||
node = self.nodes[node_name]
|
|
||||||
iq.reply().setPayload(node.info.xml).send()
|
|
||||||
else:
|
|
||||||
log.debug("Node %s requested, but does not exist." % node_name)
|
|
||||||
iq.reply().error().setPayload(iq['disco_info'].xml)
|
|
||||||
iq['error']['code'] = '404'
|
|
||||||
iq['error']['type'] = 'cancel'
|
|
||||||
iq['error']['condition'] = 'item-not-found'
|
|
||||||
iq.send()
|
|
||||||
|
|
||||||
def handle_disco_items(self, iq, forwarded=False):
|
|
||||||
"""
|
|
||||||
A default handler for disco#items requests. If another
|
|
||||||
handler is registered, this one will defer and not run.
|
|
||||||
|
|
||||||
If this handler is called by your own custom handler with
|
|
||||||
forwarded set to True, then it will run as normal.
|
|
||||||
"""
|
|
||||||
if not forwarded and self.xmpp.event_handled('disco_items_request'):
|
|
||||||
return
|
|
||||||
|
|
||||||
node_name = iq['disco_items']['node']
|
|
||||||
if not node_name:
|
|
||||||
node_name = 'main'
|
|
||||||
|
|
||||||
log.debug("Using default handler for disco#items on node '%s'." % node_name)
|
|
||||||
|
|
||||||
if node_name in self.nodes:
|
|
||||||
node = self.nodes[node_name]
|
|
||||||
iq.reply().setPayload(node.items.xml).send()
|
|
||||||
else:
|
|
||||||
log.debug("Node %s requested, but does not exist." % node_name)
|
|
||||||
iq.reply().error().setPayload(iq['disco_items'].xml)
|
|
||||||
iq['error']['code'] = '404'
|
|
||||||
iq['error']['type'] = 'cancel'
|
|
||||||
iq['error']['condition'] = 'item-not-found'
|
|
||||||
iq.send()
|
|
||||||
|
|
||||||
# Older interface methods for backwards compatibility
|
|
||||||
|
|
||||||
def getInfo(self, jid, node='', dfrom=None):
|
|
||||||
iq = self.xmpp.Iq()
|
|
||||||
iq['type'] = 'get'
|
|
||||||
iq['to'] = jid
|
|
||||||
iq['from'] = dfrom
|
|
||||||
iq['disco_info']['node'] = node
|
|
||||||
return iq.send()
|
|
||||||
|
|
||||||
def getItems(self, jid, node='', dfrom=None):
|
|
||||||
iq = self.xmpp.Iq()
|
|
||||||
iq['type'] = 'get'
|
|
||||||
iq['to'] = jid
|
|
||||||
iq['from'] = dfrom
|
|
||||||
iq['disco_items']['node'] = node
|
|
||||||
return iq.send()
|
|
||||||
|
|
||||||
def add_feature(self, feature, node='main'):
|
|
||||||
self.add_node(node)
|
|
||||||
self.nodes[node].addFeature(feature)
|
|
||||||
|
|
||||||
def add_identity(self, category='', itype='', name='', node='main'):
|
|
||||||
self.add_node(node)
|
|
||||||
self.nodes[node].addIdentity(category=category,
|
|
||||||
id_type=itype,
|
|
||||||
name=name)
|
|
||||||
|
|
||||||
def add_item(self, jid=None, name='', node='main', subnode=''):
|
|
||||||
self.add_node(node)
|
|
||||||
self.add_node(subnode)
|
|
||||||
if jid is None:
|
|
||||||
jid = self.xmpp.fulljid
|
|
||||||
self.nodes[node].addItem(jid=jid, name=name, node=subnode)
|
|
||||||
12
sleekxmpp/plugins/xep_0030/__init__.py
Normal file
12
sleekxmpp/plugins/xep_0030/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0030 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems
|
||||||
|
from sleekxmpp.plugins.xep_0030.static import StaticDisco
|
||||||
|
from sleekxmpp.plugins.xep_0030.disco import xep_0030
|
||||||
623
sleekxmpp/plugins/xep_0030/disco.py
Normal file
623
sleekxmpp/plugins/xep_0030/disco.py
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.exceptions import XMPPError
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
|
||||||
|
from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0030(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0030: Service Discovery
|
||||||
|
|
||||||
|
Service discovery in XMPP allows entities to discover information about
|
||||||
|
other agents in the network, such as the feature sets supported by a
|
||||||
|
client, or signposts to other, related entities.
|
||||||
|
|
||||||
|
Also see <http://www.xmpp.org/extensions/xep-0030.html>.
|
||||||
|
|
||||||
|
The XEP-0030 plugin works using a hierarchy of dynamic
|
||||||
|
node handlers, ranging from global handlers to specific
|
||||||
|
JID+node handlers. The default set of handlers operate
|
||||||
|
in a static manner, storing disco information in memory.
|
||||||
|
However, custom handlers may use any available backend
|
||||||
|
storage mechanism desired, such as SQLite or Redis.
|
||||||
|
|
||||||
|
Node handler hierarchy:
|
||||||
|
JID | Node | Level
|
||||||
|
---------------------
|
||||||
|
None | None | Global
|
||||||
|
Given | None | All nodes for the JID
|
||||||
|
None | Given | Node on self.xmpp.boundjid
|
||||||
|
Given | Given | A single node
|
||||||
|
|
||||||
|
Stream Handlers:
|
||||||
|
Disco Info -- Any Iq stanze that includes a query with the
|
||||||
|
namespace http://jabber.org/protocol/disco#info.
|
||||||
|
Disco Items -- Any Iq stanze that includes a query with the
|
||||||
|
namespace http://jabber.org/protocol/disco#items.
|
||||||
|
|
||||||
|
Events:
|
||||||
|
disco_info -- Received a disco#info Iq query result.
|
||||||
|
disco_items -- Received a disco#items Iq query result.
|
||||||
|
disco_info_query -- Received a disco#info Iq query request.
|
||||||
|
disco_items_query -- Received a disco#items Iq query request.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
stanza -- A reference to the module containing the
|
||||||
|
stanza classes provided by this plugin.
|
||||||
|
static -- Object containing the default set of
|
||||||
|
static node handlers.
|
||||||
|
default_handlers -- A dictionary mapping operations to the default
|
||||||
|
global handler (by default, the static handlers).
|
||||||
|
xmpp -- The main SleekXMPP object.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
set_node_handler -- Assign a handler to a JID/node combination.
|
||||||
|
del_node_handler -- Remove a handler from a JID/node combination.
|
||||||
|
get_info -- Retrieve disco#info data, locally or remote.
|
||||||
|
get_items -- Retrieve disco#items data, locally or remote.
|
||||||
|
set_identities --
|
||||||
|
set_features --
|
||||||
|
set_items --
|
||||||
|
del_items --
|
||||||
|
del_identity --
|
||||||
|
del_feature --
|
||||||
|
del_item --
|
||||||
|
add_identity --
|
||||||
|
add_feature --
|
||||||
|
add_item --
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""
|
||||||
|
Start the XEP-0030 plugin.
|
||||||
|
"""
|
||||||
|
self.xep = '0030'
|
||||||
|
self.description = 'Service Discovery'
|
||||||
|
self.stanza = sleekxmpp.plugins.xep_0030.stanza
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('Disco Info',
|
||||||
|
StanzaPath('iq/disco_info'),
|
||||||
|
self._handle_disco_info))
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('Disco Items',
|
||||||
|
StanzaPath('iq/disco_items'),
|
||||||
|
self._handle_disco_items))
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, DiscoInfo)
|
||||||
|
register_stanza_plugin(Iq, DiscoItems)
|
||||||
|
|
||||||
|
self.static = StaticDisco(self.xmpp)
|
||||||
|
|
||||||
|
self._disco_ops = ['get_info', 'set_identities', 'set_features',
|
||||||
|
'get_items', 'set_items', 'del_items',
|
||||||
|
'add_identity', 'del_identity', 'add_feature',
|
||||||
|
'del_feature', 'add_item', 'del_item',
|
||||||
|
'del_identities', 'del_features']
|
||||||
|
self.default_handlers = {}
|
||||||
|
self._handlers = {}
|
||||||
|
for op in self._disco_ops:
|
||||||
|
self._add_disco_op(op, getattr(self.static, op))
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Handle cross-plugin dependencies."""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
if 'xep_0059' in self.xmpp.plugin:
|
||||||
|
register_stanza_plugin(DiscoItems,
|
||||||
|
self.xmpp['xep_0059'].stanza.Set)
|
||||||
|
|
||||||
|
def _add_disco_op(self, op, default_handler):
|
||||||
|
self.default_handlers[op] = default_handler
|
||||||
|
self._handlers[op] = {'global': default_handler,
|
||||||
|
'jid': {},
|
||||||
|
'node': {}}
|
||||||
|
|
||||||
|
def set_node_handler(self, htype, jid=None, node=None, handler=None):
|
||||||
|
"""
|
||||||
|
Add a node handler for the given hierarchy level and
|
||||||
|
handler type.
|
||||||
|
|
||||||
|
Node handlers are ordered in a hierarchy where the
|
||||||
|
most specific handler is executed. Thus, a fallback,
|
||||||
|
global handler can be used for the majority of cases
|
||||||
|
with a few node specific handler that override the
|
||||||
|
global behavior.
|
||||||
|
|
||||||
|
Node handler hierarchy:
|
||||||
|
JID | Node | Level
|
||||||
|
---------------------
|
||||||
|
None | None | Global
|
||||||
|
Given | None | All nodes for the JID
|
||||||
|
None | Given | Node on self.xmpp.boundjid
|
||||||
|
Given | Given | A single node
|
||||||
|
|
||||||
|
Handler types:
|
||||||
|
get_info
|
||||||
|
get_items
|
||||||
|
set_identities
|
||||||
|
set_features
|
||||||
|
set_items
|
||||||
|
del_items
|
||||||
|
del_identities
|
||||||
|
del_identity
|
||||||
|
del_feature
|
||||||
|
del_features
|
||||||
|
del_item
|
||||||
|
add_identity
|
||||||
|
add_feature
|
||||||
|
add_item
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
htype -- The operation provided by the handler.
|
||||||
|
jid -- The JID the handler applies to. May be narrowed
|
||||||
|
further if a node is given.
|
||||||
|
node -- The particular node the handler is for. If no JID
|
||||||
|
is given, then the self.xmpp.boundjid.full is
|
||||||
|
assumed.
|
||||||
|
handler -- The handler function to use.
|
||||||
|
"""
|
||||||
|
if htype not in self._disco_ops:
|
||||||
|
return
|
||||||
|
if jid is None and node is None:
|
||||||
|
self._handlers[htype]['global'] = handler
|
||||||
|
elif node is None:
|
||||||
|
self._handlers[htype]['jid'][jid] = handler
|
||||||
|
elif jid is None:
|
||||||
|
if self.xmpp.is_component:
|
||||||
|
jid = self.xmpp.boundjid.full
|
||||||
|
else:
|
||||||
|
jid = self.xmpp.boundjid.bare
|
||||||
|
self._handlers[htype]['node'][(jid, node)] = handler
|
||||||
|
else:
|
||||||
|
self._handlers[htype]['node'][(jid, node)] = handler
|
||||||
|
|
||||||
|
def del_node_handler(self, htype, jid, node):
|
||||||
|
"""
|
||||||
|
Remove a handler type for a JID and node combination.
|
||||||
|
|
||||||
|
The next handler in the hierarchy will be used if one
|
||||||
|
exists. If removing the global handler, make sure that
|
||||||
|
other handlers exist to process existing nodes.
|
||||||
|
|
||||||
|
Node handler hierarchy:
|
||||||
|
JID | Node | Level
|
||||||
|
---------------------
|
||||||
|
None | None | Global
|
||||||
|
Given | None | All nodes for the JID
|
||||||
|
None | Given | Node on self.xmpp.boundjid
|
||||||
|
Given | Given | A single node
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
htype -- The type of handler to remove.
|
||||||
|
jid -- The JID from which to remove the handler.
|
||||||
|
node -- The node from which to remove the handler.
|
||||||
|
"""
|
||||||
|
self.set_node_handler(htype, jid, node, None)
|
||||||
|
|
||||||
|
def restore_defaults(self, jid=None, node=None, handlers=None):
|
||||||
|
"""
|
||||||
|
Change all or some of a node's handlers to the default
|
||||||
|
handlers. Useful for manually overriding the contents
|
||||||
|
of a node that would otherwise be handled by a JID level
|
||||||
|
or global level dynamic handler.
|
||||||
|
|
||||||
|
The default is to use the built-in static handlers, but that
|
||||||
|
may be changed by modifying self.default_handlers.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID owning the node to modify.
|
||||||
|
node -- The node to change to using static handlers.
|
||||||
|
handlers -- Optional list of handlers to change to the
|
||||||
|
default version. If provided, only these
|
||||||
|
handlers will be changed. Otherwise, all
|
||||||
|
handlers will use the default version.
|
||||||
|
"""
|
||||||
|
if handlers is None:
|
||||||
|
handlers = self._disco_ops
|
||||||
|
for op in handlers:
|
||||||
|
self.del_node_handler(op, jid, node)
|
||||||
|
self.set_node_handler(op, jid, node, self.default_handlers[op])
|
||||||
|
|
||||||
|
def get_info(self, jid=None, node=None, local=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Retrieve the disco#info results from a given JID/node combination.
|
||||||
|
|
||||||
|
Info may be retrieved from both local resources and remote agents;
|
||||||
|
the local parameter indicates if the information should be gathered
|
||||||
|
by executing the local node handlers, or if a disco#info stanza
|
||||||
|
must be generated and sent.
|
||||||
|
|
||||||
|
If requesting items from a local JID/node, then only a DiscoInfo
|
||||||
|
stanza will be returned. Otherwise, an Iq stanza will be returned.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- Request info from this JID.
|
||||||
|
node -- The particular node to query.
|
||||||
|
local -- If true, then the query is for a JID/node
|
||||||
|
combination handled by this Sleek instance and
|
||||||
|
no stanzas need to be sent.
|
||||||
|
Otherwise, a disco stanza must be sent to the
|
||||||
|
remove JID to retrieve the info.
|
||||||
|
ifrom -- Specifiy the sender's JID.
|
||||||
|
block -- If true, block and wait for the stanzas' reply.
|
||||||
|
timeout -- The time in seconds to block while waiting for
|
||||||
|
a reply. If None, then wait indefinitely. The
|
||||||
|
timeout value is only used when block=True.
|
||||||
|
callback -- Optional callback to execute when a reply is
|
||||||
|
received instead of blocking and waiting for
|
||||||
|
the reply.
|
||||||
|
"""
|
||||||
|
if local or jid is None:
|
||||||
|
log.debug("Looking up local disco#info data " + \
|
||||||
|
"for %s, node %s." % (jid, node))
|
||||||
|
info = self._run_node_handler('get_info', jid, node, kwargs)
|
||||||
|
return self._fix_default_info(info)
|
||||||
|
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
# Check dfrom parameter for backwards compatibility
|
||||||
|
iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
|
||||||
|
iq['to'] = jid
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['disco_info']['node'] = node if node else ''
|
||||||
|
return iq.send(timeout=kwargs.get('timeout', None),
|
||||||
|
block=kwargs.get('block', True),
|
||||||
|
callback=kwargs.get('callback', None))
|
||||||
|
|
||||||
|
def get_items(self, jid=None, node=None, local=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Retrieve the disco#items results from a given JID/node combination.
|
||||||
|
|
||||||
|
Items may be retrieved from both local resources and remote agents;
|
||||||
|
the local parameter indicates if the items should be gathered by
|
||||||
|
executing the local node handlers, or if a disco#items stanza must
|
||||||
|
be generated and sent.
|
||||||
|
|
||||||
|
If requesting items from a local JID/node, then only a DiscoItems
|
||||||
|
stanza will be returned. Otherwise, an Iq stanza will be returned.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- Request info from this JID.
|
||||||
|
node -- The particular node to query.
|
||||||
|
local -- If true, then the query is for a JID/node
|
||||||
|
combination handled by this Sleek instance and
|
||||||
|
no stanzas need to be sent.
|
||||||
|
Otherwise, a disco stanza must be sent to the
|
||||||
|
remove JID to retrieve the items.
|
||||||
|
ifrom -- Specifiy the sender's JID.
|
||||||
|
block -- If true, block and wait for the stanzas' reply.
|
||||||
|
timeout -- The time in seconds to block while waiting for
|
||||||
|
a reply. If None, then wait indefinitely.
|
||||||
|
callback -- Optional callback to execute when a reply is
|
||||||
|
received instead of blocking and waiting for
|
||||||
|
the reply.
|
||||||
|
iterator -- If True, return a result set iterator using
|
||||||
|
the XEP-0059 plugin, if the plugin is loaded.
|
||||||
|
Otherwise the parameter is ignored.
|
||||||
|
"""
|
||||||
|
if local or jid is None:
|
||||||
|
return self._run_node_handler('get_items', jid, node, kwargs)
|
||||||
|
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
# Check dfrom parameter for backwards compatibility
|
||||||
|
iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
|
||||||
|
iq['to'] = jid
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['disco_items']['node'] = node if node else ''
|
||||||
|
if kwargs.get('iterator', False) and self.xmpp['xep_0059']:
|
||||||
|
return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
|
||||||
|
else:
|
||||||
|
return iq.send(timeout=kwargs.get('timeout', None),
|
||||||
|
block=kwargs.get('block', True),
|
||||||
|
callback=kwargs.get('callback', None))
|
||||||
|
|
||||||
|
def set_items(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Set or replace all items for the specified JID/node combination.
|
||||||
|
|
||||||
|
The given items must be in a list or set where each item is a
|
||||||
|
tuple of the form: (jid, node, name).
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- Optional node to modify.
|
||||||
|
items -- A series of items in tuple format.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('set_items', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_items(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove all items from the given JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- Optional node to modify.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_items', jid, node, kwargs)
|
||||||
|
|
||||||
|
def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
|
||||||
|
"""
|
||||||
|
Add a new item element to the given JID/node combination.
|
||||||
|
|
||||||
|
Each item is required to have a JID, but may also specify
|
||||||
|
a node value to reference non-addressable entities.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID for the item.
|
||||||
|
name -- Optional name for the item.
|
||||||
|
node -- The node to modify.
|
||||||
|
subnode -- Optional node for the item.
|
||||||
|
ijid -- The JID to modify.
|
||||||
|
"""
|
||||||
|
if not jid:
|
||||||
|
jid = self.xmpp.boundjid.full
|
||||||
|
kwargs = {'ijid': jid,
|
||||||
|
'name': name,
|
||||||
|
'inode': subnode}
|
||||||
|
self._run_node_handler('add_item', ijid, node, kwargs)
|
||||||
|
|
||||||
|
def del_item(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove a single item from the given JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
ijid -- The item's JID.
|
||||||
|
inode -- The item's node.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_item', jid, node, kwargs)
|
||||||
|
|
||||||
|
def add_identity(self, category='', itype='', name='',
|
||||||
|
node=None, jid=None, lang=None):
|
||||||
|
"""
|
||||||
|
Add a new identity to the given JID/node combination.
|
||||||
|
|
||||||
|
Each identity must be unique in terms of all four identity
|
||||||
|
components: category, type, name, and language.
|
||||||
|
|
||||||
|
Multiple, identical category/type pairs are allowed only
|
||||||
|
if the xml:lang values are different. Likewise, multiple
|
||||||
|
category/type/xml:lang pairs are allowed so long as the
|
||||||
|
names are different. A category and type is always required.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
category -- The identity's category.
|
||||||
|
itype -- The identity's type.
|
||||||
|
name -- Optional name for the identity.
|
||||||
|
lang -- Optional two-letter language code.
|
||||||
|
node -- The node to modify.
|
||||||
|
jid -- The JID to modify.
|
||||||
|
"""
|
||||||
|
kwargs = {'category': category,
|
||||||
|
'itype': itype,
|
||||||
|
'name': name,
|
||||||
|
'lang': lang}
|
||||||
|
self._run_node_handler('add_identity', jid, node, kwargs)
|
||||||
|
|
||||||
|
def add_feature(self, feature, node=None, jid=None):
|
||||||
|
"""
|
||||||
|
Add a feature to a JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
feature -- The namespace of the supported feature.
|
||||||
|
node -- The node to modify.
|
||||||
|
jid -- The JID to modify.
|
||||||
|
"""
|
||||||
|
kwargs = {'feature': feature}
|
||||||
|
self._run_node_handler('add_feature', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_identity(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove an identity from the given JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
category -- The identity's category.
|
||||||
|
itype -- The identity's type value.
|
||||||
|
name -- Optional, human readable name for the identity.
|
||||||
|
lang -- Optional, the identity's xml:lang value.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_identity', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_feature(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove a feature from a given JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
feature -- The feature's namespace.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_feature', jid, node, kwargs)
|
||||||
|
|
||||||
|
def set_identities(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Add or replace all identities for the given JID/node combination.
|
||||||
|
|
||||||
|
The identities must be in a set where each identity is a tuple
|
||||||
|
of the form: (category, type, lang, name)
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
identities -- A set of identities in tuple form.
|
||||||
|
lang -- Optional, xml:lang value.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('set_identities', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_identities(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove all identities for a JID/node combination.
|
||||||
|
|
||||||
|
If a language is specified, only identities using that
|
||||||
|
language will be removed.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
lang -- Optional. If given, only remove identities
|
||||||
|
using this xml:lang value.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_identities', jid, node, kwargs)
|
||||||
|
|
||||||
|
def set_features(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Add or replace the set of supported features
|
||||||
|
for a JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
features -- The new set of supported features.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('set_features', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_features(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove all features from a JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_features', jid, node, kwargs)
|
||||||
|
|
||||||
|
def _run_node_handler(self, htype, jid, node, data={}):
|
||||||
|
"""
|
||||||
|
Execute the most specific node handler for the given
|
||||||
|
JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
htype -- The handler type to execute.
|
||||||
|
jid -- The JID requested.
|
||||||
|
node -- The node requested.
|
||||||
|
data -- Optional, custom data to pass to the handler.
|
||||||
|
"""
|
||||||
|
if jid is None:
|
||||||
|
if self.xmpp.is_component:
|
||||||
|
jid = self.xmpp.boundjid.full
|
||||||
|
else:
|
||||||
|
jid = self.xmpp.boundjid.bare
|
||||||
|
if node is None:
|
||||||
|
node = ''
|
||||||
|
|
||||||
|
if self._handlers[htype]['node'].get((jid, node), False):
|
||||||
|
return self._handlers[htype]['node'][(jid, node)](jid, node, data)
|
||||||
|
elif self._handlers[htype]['jid'].get(jid, False):
|
||||||
|
return self._handlers[htype]['jid'][jid](jid, node, data)
|
||||||
|
elif self._handlers[htype]['global']:
|
||||||
|
return self._handlers[htype]['global'](jid, node, data)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _handle_disco_info(self, iq):
|
||||||
|
"""
|
||||||
|
Process an incoming disco#info stanza. If it is a get
|
||||||
|
request, find and return the appropriate identities
|
||||||
|
and features. If it is an info result, fire the
|
||||||
|
disco_info event.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The incoming disco#items stanza.
|
||||||
|
"""
|
||||||
|
if iq['type'] == 'get':
|
||||||
|
log.debug("Received disco info query from " + \
|
||||||
|
"<%s> to <%s>." % (iq['from'], iq['to']))
|
||||||
|
if self.xmpp.is_component:
|
||||||
|
jid = iq['to'].full
|
||||||
|
else:
|
||||||
|
jid = iq['to'].bare
|
||||||
|
info = self._run_node_handler('get_info',
|
||||||
|
jid,
|
||||||
|
iq['disco_info']['node'],
|
||||||
|
iq)
|
||||||
|
iq.reply()
|
||||||
|
if info:
|
||||||
|
info = self._fix_default_info(info)
|
||||||
|
iq.set_payload(info.xml)
|
||||||
|
iq.send()
|
||||||
|
elif iq['type'] == 'result':
|
||||||
|
log.debug("Received disco info result from" + \
|
||||||
|
"%s to %s." % (iq['from'], iq['to']))
|
||||||
|
self.xmpp.event('disco_info', iq)
|
||||||
|
|
||||||
|
def _handle_disco_items(self, iq):
|
||||||
|
"""
|
||||||
|
Process an incoming disco#items stanza. If it is a get
|
||||||
|
request, find and return the appropriate items. If it
|
||||||
|
is an items result, fire the disco_items event.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The incoming disco#items stanza.
|
||||||
|
"""
|
||||||
|
if iq['type'] == 'get':
|
||||||
|
log.debug("Received disco items query from " + \
|
||||||
|
"<%s> to <%s>." % (iq['from'], iq['to']))
|
||||||
|
if self.xmpp.is_component:
|
||||||
|
jid = iq['to'].full
|
||||||
|
else:
|
||||||
|
jid = iq['to'].bare
|
||||||
|
items = self._run_node_handler('get_items',
|
||||||
|
jid,
|
||||||
|
iq['disco_items']['node'])
|
||||||
|
iq.reply()
|
||||||
|
if items:
|
||||||
|
iq.set_payload(items.xml)
|
||||||
|
iq.send()
|
||||||
|
elif iq['type'] == 'result':
|
||||||
|
log.debug("Received disco items result from" + \
|
||||||
|
"%s to %s." % (iq['from'], iq['to']))
|
||||||
|
self.xmpp.event('disco_items', iq)
|
||||||
|
|
||||||
|
def _fix_default_info(self, info):
|
||||||
|
"""
|
||||||
|
Disco#info results for a JID are required to include at least
|
||||||
|
one identity and feature. As a default, if no other identity is
|
||||||
|
provided, SleekXMPP will use either the generic component or the
|
||||||
|
bot client identity. A the standard disco#info feature will also be
|
||||||
|
added if no features are provided.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
info -- The disco#info quest (not the full Iq stanza) to modify.
|
||||||
|
"""
|
||||||
|
if not info['node']:
|
||||||
|
if not info['identities']:
|
||||||
|
if self.xmpp.is_component:
|
||||||
|
log.debug("No identity found for this entity." + \
|
||||||
|
"Using default component identity.")
|
||||||
|
info.add_identity('component', 'generic')
|
||||||
|
else:
|
||||||
|
log.debug("No identity found for this entity." + \
|
||||||
|
"Using default client identity.")
|
||||||
|
info.add_identity('client', 'bot')
|
||||||
|
if not info['features']:
|
||||||
|
log.debug("No features found for this entity." + \
|
||||||
|
"Using default disco#info feature.")
|
||||||
|
info.add_feature(info.namespace)
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
# Retain some backwards compatibility
|
||||||
|
xep_0030.getInfo = xep_0030.get_info
|
||||||
|
xep_0030.getItems = xep_0030.get_items
|
||||||
|
xep_0030.make_static = xep_0030.restore_defaults
|
||||||
10
sleekxmpp/plugins/xep_0030/stanza/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0030/stanza/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0030.stanza.info import DiscoInfo
|
||||||
|
from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems
|
||||||
262
sleekxmpp/plugins/xep_0030/stanza/info.py
Normal file
262
sleekxmpp/plugins/xep_0030/stanza/info.py
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoInfo(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XMPP allows for users and agents to find the identities and features
|
||||||
|
supported by other entities in the XMPP network through service discovery,
|
||||||
|
or "disco". In particular, the "disco#info" query type for <iq> stanzas is
|
||||||
|
used to request the list of identities and features offered by a JID.
|
||||||
|
|
||||||
|
An identity is a combination of a category and type, such as the 'client'
|
||||||
|
category with a type of 'pc' to indicate the agent is a human operated
|
||||||
|
client with a GUI, or a category of 'gateway' with a type of 'aim' to
|
||||||
|
identify the agent as a gateway for the legacy AIM protocol. See
|
||||||
|
<http://xmpp.org/registrar/disco-categories.html> for a full list of
|
||||||
|
accepted category and type combinations.
|
||||||
|
|
||||||
|
Features are simply a set of the namespaces that identify the supported
|
||||||
|
features. For example, a client that supports service discovery will
|
||||||
|
include the feature 'http://jabber.org/protocol/disco#info'.
|
||||||
|
|
||||||
|
Since clients and components may operate in several roles at once, identity
|
||||||
|
and feature information may be grouped into "nodes". If one were to write
|
||||||
|
all of the identities and features used by a client, then node names would
|
||||||
|
be like section headings.
|
||||||
|
|
||||||
|
Example disco#info stanzas:
|
||||||
|
<iq type="get">
|
||||||
|
<query xmlns="http://jabber.org/protocol/disco#info" />
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
<iq type="result">
|
||||||
|
<query xmlns="http://jabber.org/protocol/disco#info">
|
||||||
|
<identity category="client" type="bot" name="SleekXMPP Bot" />
|
||||||
|
<feature var="http://jabber.org/protocol/disco#info" />
|
||||||
|
<feature var="jabber:x:data" />
|
||||||
|
<feature var="urn:xmpp:ping" />
|
||||||
|
</query>
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
node -- The name of the node to either
|
||||||
|
query or return info from.
|
||||||
|
identities -- A set of 4-tuples, where each tuple contains
|
||||||
|
the category, type, xml:lang, and name
|
||||||
|
of an identity.
|
||||||
|
features -- A set of namespaces for features.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
add_identity -- Add a new, single identity.
|
||||||
|
del_identity -- Remove a single identity.
|
||||||
|
get_identities -- Return all identities in tuple form.
|
||||||
|
set_identities -- Use multiple identities, each given in tuple form.
|
||||||
|
del_identities -- Remove all identities.
|
||||||
|
add_feature -- Add a single feature.
|
||||||
|
del_feature -- Remove a single feature.
|
||||||
|
get_features -- Return a list of all features.
|
||||||
|
set_features -- Use a given list of features.
|
||||||
|
del_features -- Remove all features.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'query'
|
||||||
|
namespace = 'http://jabber.org/protocol/disco#info'
|
||||||
|
plugin_attrib = 'disco_info'
|
||||||
|
interfaces = set(('node', 'features', 'identities'))
|
||||||
|
lang_interfaces = set(('identities',))
|
||||||
|
|
||||||
|
# Cache identities and features
|
||||||
|
_identities = set()
|
||||||
|
_features = set()
|
||||||
|
|
||||||
|
def setup(self, xml=None):
|
||||||
|
"""
|
||||||
|
Populate the stanza object using an optional XML object.
|
||||||
|
|
||||||
|
Overrides ElementBase.setup
|
||||||
|
|
||||||
|
Caches identity and feature information.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
xml -- Use an existing XML object for the stanza's values.
|
||||||
|
"""
|
||||||
|
ElementBase.setup(self, xml)
|
||||||
|
|
||||||
|
self._identities = set([id[0:3] for id in self['identities']])
|
||||||
|
self._features = self['features']
|
||||||
|
|
||||||
|
def add_identity(self, category, itype, name=None, lang=None):
|
||||||
|
"""
|
||||||
|
Add a new identity element. Each identity must be unique
|
||||||
|
in terms of all four identity components.
|
||||||
|
|
||||||
|
Multiple, identical category/type pairs are allowed only
|
||||||
|
if the xml:lang values are different. Likewise, multiple
|
||||||
|
category/type/xml:lang pairs are allowed so long as the names
|
||||||
|
are different. In any case, a category and type are required.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
category -- The general category to which the agent belongs.
|
||||||
|
itype -- A more specific designation with the category.
|
||||||
|
name -- Optional human readable name for this identity.
|
||||||
|
lang -- Optional standard xml:lang value.
|
||||||
|
"""
|
||||||
|
identity = (category, itype, lang)
|
||||||
|
if identity not in self._identities:
|
||||||
|
self._identities.add(identity)
|
||||||
|
id_xml = ET.Element('{%s}identity' % self.namespace)
|
||||||
|
id_xml.attrib['category'] = category
|
||||||
|
id_xml.attrib['type'] = itype
|
||||||
|
if lang:
|
||||||
|
id_xml.attrib['{%s}lang' % self.xml_ns] = lang
|
||||||
|
if name:
|
||||||
|
id_xml.attrib['name'] = name
|
||||||
|
self.xml.append(id_xml)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def del_identity(self, category, itype, name=None, lang=None):
|
||||||
|
"""
|
||||||
|
Remove a given identity.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
category -- The general category to which the agent belonged.
|
||||||
|
itype -- A more specific designation with the category.
|
||||||
|
name -- Optional human readable name for this identity.
|
||||||
|
lang -- Optional, standard xml:lang value.
|
||||||
|
"""
|
||||||
|
identity = (category, itype, lang)
|
||||||
|
if identity in self._identities:
|
||||||
|
self._identities.remove(identity)
|
||||||
|
for id_xml in self.findall('{%s}identity' % self.namespace):
|
||||||
|
id = (id_xml.attrib['category'],
|
||||||
|
id_xml.attrib['type'],
|
||||||
|
id_xml.attrib.get('{%s}lang' % self.xml_ns, None))
|
||||||
|
if id == identity:
|
||||||
|
self.xml.remove(id_xml)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_identities(self, lang=None):
|
||||||
|
"""
|
||||||
|
Return a set of all identities in tuple form as so:
|
||||||
|
(category, type, lang, name)
|
||||||
|
|
||||||
|
If a language was specified, only return identities using
|
||||||
|
that language.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
lang -- Optional, standard xml:lang value.
|
||||||
|
"""
|
||||||
|
identities = set()
|
||||||
|
for id_xml in self.findall('{%s}identity' % self.namespace):
|
||||||
|
xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
|
||||||
|
if lang is None or xml_lang == lang:
|
||||||
|
identities.add((
|
||||||
|
id_xml.attrib['category'],
|
||||||
|
id_xml.attrib['type'],
|
||||||
|
id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
|
||||||
|
id_xml.attrib.get('name', None)))
|
||||||
|
return identities
|
||||||
|
|
||||||
|
def set_identities(self, identities, lang=None):
|
||||||
|
"""
|
||||||
|
Add or replace all identities. The identities must be a in set
|
||||||
|
where each identity is a tuple of the form:
|
||||||
|
(category, type, lang, name)
|
||||||
|
|
||||||
|
If a language is specifified, any identities using that language
|
||||||
|
will be removed to be replaced with the given identities.
|
||||||
|
|
||||||
|
NOTE: An identity's language will not be changed regardless of
|
||||||
|
the value of lang.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
identities -- A set of identities in tuple form.
|
||||||
|
lang -- Optional, standard xml:lang value.
|
||||||
|
"""
|
||||||
|
self.del_identities(lang)
|
||||||
|
for identity in identities:
|
||||||
|
category, itype, lang, name = identity
|
||||||
|
self.add_identity(category, itype, name, lang)
|
||||||
|
|
||||||
|
def del_identities(self, lang=None):
|
||||||
|
"""
|
||||||
|
Remove all identities. If a language was specified, only
|
||||||
|
remove identities using that language.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
lang -- Optional, standard xml:lang value.
|
||||||
|
"""
|
||||||
|
for id_xml in self.findall('{%s}identity' % self.namespace):
|
||||||
|
if lang is None:
|
||||||
|
self.xml.remove(id_xml)
|
||||||
|
elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang:
|
||||||
|
self._identities.remove((
|
||||||
|
id_xml.attrib['category'],
|
||||||
|
id_xml.attrib['type'],
|
||||||
|
id_xml.attrib.get('{%s}lang' % self.xml_ns, None)))
|
||||||
|
self.xml.remove(id_xml)
|
||||||
|
|
||||||
|
def add_feature(self, feature):
|
||||||
|
"""
|
||||||
|
Add a single, new feature.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
feature -- The namespace of the supported feature.
|
||||||
|
"""
|
||||||
|
if feature not in self._features:
|
||||||
|
self._features.add(feature)
|
||||||
|
feature_xml = ET.Element('{%s}feature' % self.namespace)
|
||||||
|
feature_xml.attrib['var'] = feature
|
||||||
|
self.xml.append(feature_xml)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def del_feature(self, feature):
|
||||||
|
"""
|
||||||
|
Remove a single feature.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
feature -- The namespace of the removed feature.
|
||||||
|
"""
|
||||||
|
if feature in self._features:
|
||||||
|
self._features.remove(feature)
|
||||||
|
for feature_xml in self.findall('{%s}feature' % self.namespace):
|
||||||
|
if feature_xml.attrib['var'] == feature:
|
||||||
|
self.xml.remove(feature_xml)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_features(self):
|
||||||
|
"""Return the set of all supported features."""
|
||||||
|
features = set()
|
||||||
|
for feature_xml in self.findall('{%s}feature' % self.namespace):
|
||||||
|
features.add(feature_xml.attrib['var'])
|
||||||
|
return features
|
||||||
|
|
||||||
|
def set_features(self, features):
|
||||||
|
"""
|
||||||
|
Add or replace the set of supported features.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
features -- The new set of supported features.
|
||||||
|
"""
|
||||||
|
self.del_features()
|
||||||
|
for feature in features:
|
||||||
|
self.add_feature(feature)
|
||||||
|
|
||||||
|
def del_features(self):
|
||||||
|
"""Remove all features."""
|
||||||
|
self._features = set()
|
||||||
|
for feature_xml in self.findall('{%s}feature' % self.namespace):
|
||||||
|
self.xml.remove(feature_xml)
|
||||||
136
sleekxmpp/plugins/xep_0030/stanza/items.py
Normal file
136
sleekxmpp/plugins/xep_0030/stanza/items.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoItems(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Example disco#items stanzas:
|
||||||
|
<iq type="get">
|
||||||
|
<query xmlns="http://jabber.org/protocol/disco#items" />
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
<iq type="result">
|
||||||
|
<query xmlns="http://jabber.org/protocol/disco#items">
|
||||||
|
<item jid="chat.example.com"
|
||||||
|
node="xmppdev"
|
||||||
|
name="XMPP Dev" />
|
||||||
|
<item jid="chat.example.com"
|
||||||
|
node="sleekdev"
|
||||||
|
name="SleekXMPP Dev" />
|
||||||
|
</query>
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
node -- The name of the node to either
|
||||||
|
query or return info from.
|
||||||
|
items -- A list of 3-tuples, where each tuple contains
|
||||||
|
the JID, node, and name of an item.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
add_item -- Add a single new item.
|
||||||
|
del_item -- Remove a single item.
|
||||||
|
get_items -- Return all items.
|
||||||
|
set_items -- Set or replace all items.
|
||||||
|
del_items -- Remove all items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'query'
|
||||||
|
namespace = 'http://jabber.org/protocol/disco#items'
|
||||||
|
plugin_attrib = 'disco_items'
|
||||||
|
interfaces = set(('node', 'items'))
|
||||||
|
|
||||||
|
# Cache items
|
||||||
|
_items = set()
|
||||||
|
|
||||||
|
def setup(self, xml=None):
|
||||||
|
"""
|
||||||
|
Populate the stanza object using an optional XML object.
|
||||||
|
|
||||||
|
Overrides ElementBase.setup
|
||||||
|
|
||||||
|
Caches item information.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
xml -- Use an existing XML object for the stanza's values.
|
||||||
|
"""
|
||||||
|
ElementBase.setup(self, xml)
|
||||||
|
self._items = set([item[0:2] for item in self['items']])
|
||||||
|
|
||||||
|
def add_item(self, jid, node=None, name=None):
|
||||||
|
"""
|
||||||
|
Add a new item element. Each item is required to have a
|
||||||
|
JID, but may also specify a node value to reference
|
||||||
|
non-addressable entitities.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID for the item.
|
||||||
|
node -- Optional additional information to reference
|
||||||
|
non-addressable items.
|
||||||
|
name -- Optional human readable name for the item.
|
||||||
|
"""
|
||||||
|
if (jid, node) not in self._items:
|
||||||
|
self._items.add((jid, node))
|
||||||
|
item_xml = ET.Element('{%s}item' % self.namespace)
|
||||||
|
item_xml.attrib['jid'] = jid
|
||||||
|
if name:
|
||||||
|
item_xml.attrib['name'] = name
|
||||||
|
if node:
|
||||||
|
item_xml.attrib['node'] = node
|
||||||
|
self.xml.append(item_xml)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def del_item(self, jid, node=None):
|
||||||
|
"""
|
||||||
|
Remove a single item.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- JID of the item to remove.
|
||||||
|
node -- Optional extra identifying information.
|
||||||
|
"""
|
||||||
|
if (jid, node) in self._items:
|
||||||
|
for item_xml in self.findall('{%s}item' % self.namespace):
|
||||||
|
item = (item_xml.attrib['jid'],
|
||||||
|
item_xml.attrib.get('node', None))
|
||||||
|
if item == (jid, node):
|
||||||
|
self.xml.remove(item_xml)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_items(self):
|
||||||
|
"""Return all items."""
|
||||||
|
items = set()
|
||||||
|
for item_xml in self.findall('{%s}item' % self.namespace):
|
||||||
|
item = (item_xml.attrib['jid'],
|
||||||
|
item_xml.attrib.get('node'),
|
||||||
|
item_xml.attrib.get('name'))
|
||||||
|
items.add(item)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def set_items(self, items):
|
||||||
|
"""
|
||||||
|
Set or replace all items. The given items must be in a
|
||||||
|
list or set where each item is a tuple of the form:
|
||||||
|
(jid, node, name)
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
items -- A series of items in tuple format.
|
||||||
|
"""
|
||||||
|
self.del_items()
|
||||||
|
for item in items:
|
||||||
|
jid, node, name = item
|
||||||
|
self.add_item(jid, node, name)
|
||||||
|
|
||||||
|
def del_items(self):
|
||||||
|
"""Remove all items."""
|
||||||
|
self._items = set()
|
||||||
|
for item_xml in self.findall('{%s}item' % self.namespace):
|
||||||
|
self.xml.remove(item_xml)
|
||||||
265
sleekxmpp/plugins/xep_0030/static.py
Normal file
265
sleekxmpp/plugins/xep_0030/static.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.exceptions import XMPPError
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
|
||||||
|
from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class StaticDisco(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
While components will likely require fully dynamic handling
|
||||||
|
of service discovery information, most clients and simple bots
|
||||||
|
only need to manage a few disco nodes that will remain mostly
|
||||||
|
static.
|
||||||
|
|
||||||
|
StaticDisco provides a set of node handlers that will store
|
||||||
|
static sets of disco info and items in memory.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
nodes -- A dictionary mapping (JID, node) tuples to a dict
|
||||||
|
containing a disco#info and a disco#items stanza.
|
||||||
|
xmpp -- The main SleekXMPP object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, xmpp):
|
||||||
|
"""
|
||||||
|
Create a static disco interface. Sets of disco#info and
|
||||||
|
disco#items are maintained for every given JID and node
|
||||||
|
combination. These stanzas are used to store disco
|
||||||
|
information in memory without any additional processing.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
xmpp -- The main SleekXMPP object.
|
||||||
|
"""
|
||||||
|
self.nodes = {}
|
||||||
|
self.xmpp = xmpp
|
||||||
|
|
||||||
|
def add_node(self, jid=None, node=None):
|
||||||
|
"""
|
||||||
|
Create a new set of stanzas for the provided
|
||||||
|
JID and node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID that will own the new stanzas.
|
||||||
|
node -- The node that will own the new stanzas.
|
||||||
|
"""
|
||||||
|
if jid is None:
|
||||||
|
jid = self.xmpp.boundjid.full
|
||||||
|
if node is None:
|
||||||
|
node = ''
|
||||||
|
if (jid, node) not in self.nodes:
|
||||||
|
self.nodes[(jid, node)] = {'info': DiscoInfo(),
|
||||||
|
'items': DiscoItems()}
|
||||||
|
self.nodes[(jid, node)]['info']['node'] = node
|
||||||
|
self.nodes[(jid, node)]['items']['node'] = node
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Node Handlers
|
||||||
|
#
|
||||||
|
# Each handler accepts three arguments: jid, node, and data.
|
||||||
|
# The jid and node parameters together determine the set of
|
||||||
|
# info and items stanzas that will be retrieved or added.
|
||||||
|
# The data parameter is a dictionary with additional paramters
|
||||||
|
# that will be passed to other calls.
|
||||||
|
|
||||||
|
def get_info(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Return the stored info data for the requested JID/node combination.
|
||||||
|
|
||||||
|
The data parameter is not used.
|
||||||
|
"""
|
||||||
|
if (jid, node) not in self.nodes:
|
||||||
|
if not node:
|
||||||
|
return DiscoInfo()
|
||||||
|
else:
|
||||||
|
raise XMPPError(condition='item-not-found')
|
||||||
|
else:
|
||||||
|
return self.nodes[(jid, node)]['info']
|
||||||
|
|
||||||
|
def del_info(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Reset the info stanza for a given JID/node combination.
|
||||||
|
|
||||||
|
The data parameter is not used.
|
||||||
|
"""
|
||||||
|
if (jid, node) in self.nodes:
|
||||||
|
self.nodes[(jid, node)]['info'] = DiscoInfo()
|
||||||
|
|
||||||
|
def get_items(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Return the stored items data for the requested JID/node combination.
|
||||||
|
|
||||||
|
The data parameter is not used.
|
||||||
|
"""
|
||||||
|
if (jid, node) not in self.nodes:
|
||||||
|
if not node:
|
||||||
|
return DiscoInfo()
|
||||||
|
else:
|
||||||
|
raise XMPPError(condition='item-not-found')
|
||||||
|
else:
|
||||||
|
return self.nodes[(jid, node)]['items']
|
||||||
|
|
||||||
|
def set_items(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Replace the stored items data for a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter may provided:
|
||||||
|
items -- A set of items in tuple format.
|
||||||
|
"""
|
||||||
|
items = data.get('items', set())
|
||||||
|
self.add_node(jid, node)
|
||||||
|
self.nodes[(jid, node)]['items']['items'] = items
|
||||||
|
|
||||||
|
def del_items(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Reset the items stanza for a given JID/node combination.
|
||||||
|
|
||||||
|
The data parameter is not used.
|
||||||
|
"""
|
||||||
|
if (jid, node) in self.nodes:
|
||||||
|
self.nodes[(jid, node)]['items'] = DiscoItems()
|
||||||
|
|
||||||
|
def add_identity(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Add a new identity to te JID/node combination.
|
||||||
|
|
||||||
|
The data parameter may provide:
|
||||||
|
category -- The general category to which the agent belongs.
|
||||||
|
itype -- A more specific designation with the category.
|
||||||
|
name -- Optional human readable name for this identity.
|
||||||
|
lang -- Optional standard xml:lang value.
|
||||||
|
"""
|
||||||
|
self.add_node(jid, node)
|
||||||
|
self.nodes[(jid, node)]['info'].add_identity(
|
||||||
|
data.get('category', ''),
|
||||||
|
data.get('itype', ''),
|
||||||
|
data.get('name', None),
|
||||||
|
data.get('lang', None))
|
||||||
|
|
||||||
|
def set_identities(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Add or replace all identities for a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter should include:
|
||||||
|
identities -- A list of identities in tuple form:
|
||||||
|
(category, type, name, lang)
|
||||||
|
"""
|
||||||
|
identities = data.get('identities', set())
|
||||||
|
self.add_node(jid, node)
|
||||||
|
self.nodes[(jid, node)]['info']['identities'] = identities
|
||||||
|
|
||||||
|
def del_identity(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Remove an identity from a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter may provide:
|
||||||
|
category -- The general category to which the agent belonged.
|
||||||
|
itype -- A more specific designation with the category.
|
||||||
|
name -- Optional human readable name for this identity.
|
||||||
|
lang -- Optional, standard xml:lang value.
|
||||||
|
"""
|
||||||
|
if (jid, node) not in self.nodes:
|
||||||
|
return
|
||||||
|
self.nodes[(jid, node)]['info'].del_identity(
|
||||||
|
data.get('category', ''),
|
||||||
|
data.get('itype', ''),
|
||||||
|
data.get('name', None),
|
||||||
|
data.get('lang', None))
|
||||||
|
|
||||||
|
def del_identities(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Remove all identities from a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter is not used.
|
||||||
|
"""
|
||||||
|
if (jid, node) not in self.nodes:
|
||||||
|
return
|
||||||
|
del self.nodes[(jid, node)]['info']['identities']
|
||||||
|
|
||||||
|
def add_feature(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Add a feature to a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter should include:
|
||||||
|
feature -- The namespace of the supported feature.
|
||||||
|
"""
|
||||||
|
self.add_node(jid, node)
|
||||||
|
self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
|
||||||
|
|
||||||
|
def set_features(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Add or replace all features for a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter should include:
|
||||||
|
features -- The new set of supported features.
|
||||||
|
"""
|
||||||
|
features = data.get('features', set())
|
||||||
|
self.add_node(jid, node)
|
||||||
|
self.nodes[(jid, node)]['info']['features'] = features
|
||||||
|
|
||||||
|
def del_feature(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Remove a feature from a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter should include:
|
||||||
|
feature -- The namespace of the removed feature.
|
||||||
|
"""
|
||||||
|
if (jid, node) not in self.nodes:
|
||||||
|
return
|
||||||
|
self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
|
||||||
|
|
||||||
|
def del_features(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Remove all features from a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter is not used.
|
||||||
|
"""
|
||||||
|
if (jid, node) not in self.nodes:
|
||||||
|
return
|
||||||
|
del self.nodes[(jid, node)]['info']['features']
|
||||||
|
|
||||||
|
def add_item(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Add an item to a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter may include:
|
||||||
|
ijid -- The JID for the item.
|
||||||
|
inode -- Optional additional information to reference
|
||||||
|
non-addressable items.
|
||||||
|
name -- Optional human readable name for the item.
|
||||||
|
"""
|
||||||
|
self.add_node(jid, node)
|
||||||
|
self.nodes[(jid, node)]['items'].add_item(
|
||||||
|
data.get('ijid', ''),
|
||||||
|
node=data.get('inode', ''),
|
||||||
|
name=data.get('name', ''))
|
||||||
|
|
||||||
|
def del_item(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Remove an item from a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter may include:
|
||||||
|
ijid -- JID of the item to remove.
|
||||||
|
inode -- Optional extra identifying information.
|
||||||
|
"""
|
||||||
|
if (jid, node) in self.nodes:
|
||||||
|
self.nodes[(jid, node)]['items'].del_item(
|
||||||
|
data.get('ijid', ''),
|
||||||
|
node=data.get('inode', None))
|
||||||
@@ -20,325 +20,334 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class MUCPresence(ElementBase):
|
class MUCPresence(ElementBase):
|
||||||
name = 'x'
|
name = 'x'
|
||||||
namespace = 'http://jabber.org/protocol/muc#user'
|
namespace = 'http://jabber.org/protocol/muc#user'
|
||||||
plugin_attrib = 'muc'
|
plugin_attrib = 'muc'
|
||||||
interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
|
interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
|
||||||
affiliations = set(('', ))
|
affiliations = set(('', ))
|
||||||
roles = set(('', ))
|
roles = set(('', ))
|
||||||
|
|
||||||
def getXMLItem(self):
|
def getXMLItem(self):
|
||||||
item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
|
item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
|
||||||
if item is None:
|
if item is None:
|
||||||
item = ET.Element('{http://jabber.org/protocol/muc#user}item')
|
item = ET.Element('{http://jabber.org/protocol/muc#user}item')
|
||||||
self.xml.append(item)
|
self.xml.append(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def getAffiliation(self):
|
def getAffiliation(self):
|
||||||
#TODO if no affilation, set it to the default and return default
|
#TODO if no affilation, set it to the default and return default
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
return item.get('affiliation', '')
|
return item.get('affiliation', '')
|
||||||
|
|
||||||
def setAffiliation(self, value):
|
def setAffiliation(self, value):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO check for valid affiliation
|
#TODO check for valid affiliation
|
||||||
item.attrib['affiliation'] = value
|
item.attrib['affiliation'] = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delAffiliation(self):
|
def delAffiliation(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO set default affiliation
|
#TODO set default affiliation
|
||||||
if 'affiliation' in item.attrib: del item.attrib['affiliation']
|
if 'affiliation' in item.attrib: del item.attrib['affiliation']
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def getJid(self):
|
def getJid(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
return JID(item.get('jid', ''))
|
return JID(item.get('jid', ''))
|
||||||
|
|
||||||
def setJid(self, value):
|
def setJid(self, value):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
value = str(value)
|
value = str(value)
|
||||||
item.attrib['jid'] = value
|
item.attrib['jid'] = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delJid(self):
|
def delJid(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
if 'jid' in item.attrib: del item.attrib['jid']
|
if 'jid' in item.attrib: del item.attrib['jid']
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def getRole(self):
|
def getRole(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO get default role, set default role if none
|
#TODO get default role, set default role if none
|
||||||
return item.get('role', '')
|
return item.get('role', '')
|
||||||
|
|
||||||
def setRole(self, value):
|
def setRole(self, value):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO check for valid role
|
#TODO check for valid role
|
||||||
item.attrib['role'] = value
|
item.attrib['role'] = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delRole(self):
|
def delRole(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO set default role
|
#TODO set default role
|
||||||
if 'role' in item.attrib: del item.attrib['role']
|
if 'role' in item.attrib: del item.attrib['role']
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def getNick(self):
|
def getNick(self):
|
||||||
return self.parent()['from'].resource
|
return self.parent()['from'].resource
|
||||||
|
|
||||||
def getRoom(self):
|
def getRoom(self):
|
||||||
return self.parent()['from'].bare
|
return self.parent()['from'].bare
|
||||||
|
|
||||||
def setNick(self, value):
|
def setNick(self, value):
|
||||||
log.warning("Cannot set nick through mucpresence plugin.")
|
log.warning("Cannot set nick through mucpresence plugin.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def setRoom(self, value):
|
def setRoom(self, value):
|
||||||
log.warning("Cannot set room through mucpresence plugin.")
|
log.warning("Cannot set room through mucpresence plugin.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delNick(self):
|
def delNick(self):
|
||||||
log.warning("Cannot delete nick through mucpresence plugin.")
|
log.warning("Cannot delete nick through mucpresence plugin.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delRoom(self):
|
def delRoom(self):
|
||||||
log.warning("Cannot delete room through mucpresence plugin.")
|
log.warning("Cannot delete room through mucpresence plugin.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
class xep_0045(base.base_plugin):
|
class xep_0045(base.base_plugin):
|
||||||
"""
|
"""
|
||||||
Impliments XEP-0045 Multi User Chat
|
Implements XEP-0045 Multi User Chat
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def plugin_init(self):
|
def plugin_init(self):
|
||||||
self.rooms = {}
|
self.rooms = {}
|
||||||
self.ourNicks = {}
|
self.ourNicks = {}
|
||||||
self.xep = '0045'
|
self.xep = '0045'
|
||||||
self.description = 'Multi User Chat'
|
self.description = 'Multi User Chat'
|
||||||
# load MUC support in presence stanzas
|
# load MUC support in presence stanzas
|
||||||
registerStanzaPlugin(Presence, MUCPresence)
|
registerStanzaPlugin(Presence, MUCPresence)
|
||||||
self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
|
self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
|
||||||
self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
|
self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
|
||||||
self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
|
self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
|
||||||
|
self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{http://jabber.org/protocol/muc#user}x/invite" % self.xmpp.default_ns), self.handle_groupchat_invite))
|
||||||
|
|
||||||
def handle_groupchat_presence(self, pr):
|
def handle_groupchat_invite(self, inv):
|
||||||
""" Handle a presence in a muc.
|
""" Handle an invite into a muc.
|
||||||
"""
|
"""
|
||||||
got_offline = False
|
logging.debug("MUC invite to %s from %s: %s" % (inv['from'], inv["from"], inv))
|
||||||
got_online = False
|
if inv['from'] not in self.rooms.keys():
|
||||||
if pr['muc']['room'] not in self.rooms.keys():
|
self.xmpp.event("groupchat_invite", inv)
|
||||||
return
|
|
||||||
entry = pr['muc'].getStanzaValues()
|
|
||||||
entry['show'] = pr['show']
|
|
||||||
entry['status'] = pr['status']
|
|
||||||
if pr['type'] == 'unavailable':
|
|
||||||
if entry['nick'] in self.rooms[entry['room']]:
|
|
||||||
del self.rooms[entry['room']][entry['nick']]
|
|
||||||
got_offline = True
|
|
||||||
else:
|
|
||||||
if entry['nick'] not in self.rooms[entry['room']]:
|
|
||||||
got_online = True
|
|
||||||
self.rooms[entry['room']][entry['nick']] = entry
|
|
||||||
log.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
|
|
||||||
self.xmpp.event("groupchat_presence", pr)
|
|
||||||
self.xmpp.event("muc::%s::presence" % entry['room'], pr)
|
|
||||||
if got_offline:
|
|
||||||
self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
|
|
||||||
if got_online:
|
|
||||||
self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
|
|
||||||
|
|
||||||
def handle_groupchat_message(self, msg):
|
def handle_groupchat_presence(self, pr):
|
||||||
""" Handle a message event in a muc.
|
""" Handle a presence in a muc.
|
||||||
"""
|
"""
|
||||||
self.xmpp.event('groupchat_message', msg)
|
got_offline = False
|
||||||
self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
|
got_online = False
|
||||||
|
if pr['muc']['room'] not in self.rooms.keys():
|
||||||
|
return
|
||||||
|
entry = pr['muc'].getStanzaValues()
|
||||||
|
entry['show'] = pr['show']
|
||||||
|
entry['status'] = pr['status']
|
||||||
|
if pr['type'] == 'unavailable':
|
||||||
|
if entry['nick'] in self.rooms[entry['room']]:
|
||||||
|
del self.rooms[entry['room']][entry['nick']]
|
||||||
|
got_offline = True
|
||||||
|
else:
|
||||||
|
if entry['nick'] not in self.rooms[entry['room']]:
|
||||||
|
got_online = True
|
||||||
|
self.rooms[entry['room']][entry['nick']] = entry
|
||||||
|
log.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
|
||||||
|
self.xmpp.event("groupchat_presence", pr)
|
||||||
|
self.xmpp.event("muc::%s::presence" % entry['room'], pr)
|
||||||
|
if got_offline:
|
||||||
|
self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
|
||||||
|
if got_online:
|
||||||
|
self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
|
||||||
|
|
||||||
def handle_groupchat_subject(self, msg):
|
def handle_groupchat_message(self, msg):
|
||||||
""" Handle a message coming from a muc indicating
|
""" Handle a message event in a muc.
|
||||||
a change of subject (or announcing it when joining the room)
|
"""
|
||||||
"""
|
self.xmpp.event('groupchat_message', msg)
|
||||||
self.xmpp.event('groupchat_subject', msg)
|
self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
|
||||||
|
|
||||||
def jidInRoom(self, room, jid):
|
def handle_groupchat_subject(self, msg):
|
||||||
for nick in self.rooms[room]:
|
""" Handle a message coming from a muc indicating
|
||||||
entry = self.rooms[room][nick]
|
a change of subject (or announcing it when joining the room)
|
||||||
if entry is not None and entry['jid'].full == jid:
|
"""
|
||||||
return True
|
self.xmpp.event('groupchat_subject', msg)
|
||||||
return False
|
|
||||||
|
|
||||||
def getNick(self, room, jid):
|
def jidInRoom(self, room, jid):
|
||||||
for nick in self.rooms[room]:
|
for nick in self.rooms[room]:
|
||||||
entry = self.rooms[room][nick]
|
entry = self.rooms[room][nick]
|
||||||
if entry is not None and entry['jid'].full == jid:
|
if entry is not None and entry['jid'].full == jid:
|
||||||
return nick
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def getRoomForm(self, room, ifrom=None):
|
def getNick(self, room, jid):
|
||||||
iq = self.xmpp.makeIqGet()
|
for nick in self.rooms[room]:
|
||||||
iq['to'] = room
|
entry = self.rooms[room][nick]
|
||||||
if ifrom is not None:
|
if entry is not None and entry['jid'].full == jid:
|
||||||
iq['from'] = ifrom
|
return nick
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
|
||||||
iq.append(query)
|
|
||||||
result = iq.send()
|
|
||||||
if result['type'] == 'error':
|
|
||||||
return False
|
|
||||||
xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
|
||||||
if xform is None: return False
|
|
||||||
form = self.xmpp.plugin['old_0004'].buildForm(xform)
|
|
||||||
return form
|
|
||||||
|
|
||||||
def configureRoom(self, room, form=None, ifrom=None):
|
def getRoomForm(self, room, ifrom=None):
|
||||||
if form is None:
|
iq = self.xmpp.makeIqGet()
|
||||||
form = self.getRoomForm(room, ifrom=ifrom)
|
iq['to'] = room
|
||||||
#form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
|
if ifrom is not None:
|
||||||
#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
|
iq['from'] = ifrom
|
||||||
iq = self.xmpp.makeIqSet()
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
iq['to'] = room
|
iq.append(query)
|
||||||
if ifrom is not None:
|
result = iq.send()
|
||||||
iq['from'] = ifrom
|
if result['type'] == 'error':
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
return False
|
||||||
form = form.getXML('submit')
|
xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
||||||
query.append(form)
|
if xform is None: return False
|
||||||
iq.append(query)
|
form = self.xmpp.plugin['old_0004'].buildForm(xform)
|
||||||
result = iq.send()
|
return form
|
||||||
if result['type'] == 'error':
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
|
def configureRoom(self, room, form=None, ifrom=None):
|
||||||
""" Join the specified room, requesting 'maxhistory' lines of history.
|
if form is None:
|
||||||
"""
|
form = self.getRoomForm(room, ifrom=ifrom)
|
||||||
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
|
#form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
|
||||||
x = ET.Element('{http://jabber.org/protocol/muc}x')
|
#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
|
||||||
if password:
|
iq = self.xmpp.makeIqSet()
|
||||||
passelement = ET.Element('password')
|
iq['to'] = room
|
||||||
passelement.text = password
|
if ifrom is not None:
|
||||||
x.append(passelement)
|
iq['from'] = ifrom
|
||||||
if maxhistory:
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
history = ET.Element('history')
|
form = form.getXML('submit')
|
||||||
if maxhistory == "0":
|
query.append(form)
|
||||||
history.attrib['maxchars'] = maxhistory
|
iq.append(query)
|
||||||
else:
|
result = iq.send()
|
||||||
history.attrib['maxstanzas'] = maxhistory
|
if result['type'] == 'error':
|
||||||
x.append(history)
|
return False
|
||||||
stanza.append(x)
|
return True
|
||||||
if not wait:
|
|
||||||
self.xmpp.send(stanza)
|
|
||||||
else:
|
|
||||||
#wait for our own room presence back
|
|
||||||
expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
|
|
||||||
self.xmpp.send(stanza, expect)
|
|
||||||
self.rooms[room] = {}
|
|
||||||
self.ourNicks[room] = nick
|
|
||||||
|
|
||||||
def destroy(self, room, reason='', altroom = '', ifrom=None):
|
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
|
||||||
iq = self.xmpp.makeIqSet()
|
""" Join the specified room, requesting 'maxhistory' lines of history.
|
||||||
if ifrom is not None:
|
"""
|
||||||
iq['from'] = ifrom
|
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
|
||||||
iq['to'] = room
|
x = ET.Element('{http://jabber.org/protocol/muc}x')
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
if password:
|
||||||
destroy = ET.Element('destroy')
|
passelement = ET.Element('password')
|
||||||
if altroom:
|
passelement.text = password
|
||||||
destroy.attrib['jid'] = altroom
|
x.append(passelement)
|
||||||
xreason = ET.Element('reason')
|
if maxhistory:
|
||||||
xreason.text = reason
|
history = ET.Element('history')
|
||||||
destroy.append(xreason)
|
if maxhistory == "0":
|
||||||
query.append(destroy)
|
history.attrib['maxchars'] = maxhistory
|
||||||
iq.append(query)
|
else:
|
||||||
r = iq.send()
|
history.attrib['maxstanzas'] = maxhistory
|
||||||
if r is False or r['type'] == 'error':
|
x.append(history)
|
||||||
return False
|
stanza.append(x)
|
||||||
return True
|
if not wait:
|
||||||
|
self.xmpp.send(stanza)
|
||||||
|
else:
|
||||||
|
#wait for our own room presence back
|
||||||
|
expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
|
||||||
|
self.xmpp.send(stanza, expect)
|
||||||
|
self.rooms[room] = {}
|
||||||
|
self.ourNicks[room] = nick
|
||||||
|
|
||||||
def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
|
def destroy(self, room, reason='', altroom = '', ifrom=None):
|
||||||
""" Change room affiliation."""
|
iq = self.xmpp.makeIqSet()
|
||||||
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
|
if ifrom is not None:
|
||||||
raise TypeError
|
iq['from'] = ifrom
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
|
iq['to'] = room
|
||||||
if nick is not None:
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
|
destroy = ET.Element('destroy')
|
||||||
else:
|
if altroom:
|
||||||
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
|
destroy.attrib['jid'] = altroom
|
||||||
query.append(item)
|
xreason = ET.Element('reason')
|
||||||
iq = self.xmpp.makeIqSet(query)
|
xreason.text = reason
|
||||||
iq['to'] = room
|
destroy.append(xreason)
|
||||||
result = iq.send()
|
query.append(destroy)
|
||||||
if result is False or result['type'] != 'result':
|
iq.append(query)
|
||||||
raise ValueError
|
r = iq.send()
|
||||||
return True
|
if r is False or r['type'] == 'error':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def invite(self, room, jid, reason=''):
|
def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
|
||||||
""" Invite a jid to a room."""
|
""" Change room affiliation."""
|
||||||
msg = self.xmpp.makeMessage(room)
|
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
|
||||||
msg['from'] = self.xmpp.jid
|
raise TypeError
|
||||||
x = ET.Element('{http://jabber.org/protocol/muc#user}x')
|
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
|
||||||
invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
|
if nick is not None:
|
||||||
if reason:
|
item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
|
||||||
rxml = ET.Element('reason')
|
else:
|
||||||
rxml.text = reason
|
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
|
||||||
invite.append(rxml)
|
query.append(item)
|
||||||
x.append(invite)
|
iq = self.xmpp.makeIqSet(query)
|
||||||
msg.append(x)
|
iq['to'] = room
|
||||||
self.xmpp.send(msg)
|
result = iq.send()
|
||||||
|
if result is False or result['type'] != 'result':
|
||||||
|
raise ValueError
|
||||||
|
return True
|
||||||
|
|
||||||
def leaveMUC(self, room, nick, msg=''):
|
def invite(self, room, jid, reason='', mfrom=''):
|
||||||
""" Leave the specified room.
|
""" Invite a jid to a room."""
|
||||||
"""
|
msg = self.xmpp.makeMessage(room)
|
||||||
if msg:
|
msg['from'] = mfrom
|
||||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
|
x = ET.Element('{http://jabber.org/protocol/muc#user}x')
|
||||||
else:
|
invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
|
||||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
|
if reason:
|
||||||
del self.rooms[room]
|
rxml = ET.Element('reason')
|
||||||
|
rxml.text = reason
|
||||||
|
invite.append(rxml)
|
||||||
|
x.append(invite)
|
||||||
|
msg.append(x)
|
||||||
|
self.xmpp.send(msg)
|
||||||
|
|
||||||
def getRoomConfig(self, room):
|
def leaveMUC(self, room, nick, msg=''):
|
||||||
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
|
""" Leave the specified room.
|
||||||
iq['to'] = room
|
"""
|
||||||
iq['from'] = self.xmpp.jid
|
if msg:
|
||||||
result = iq.send()
|
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
|
||||||
if result is None or result['type'] != 'result':
|
else:
|
||||||
raise ValueError
|
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
|
||||||
form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
del self.rooms[room]
|
||||||
if form is None:
|
|
||||||
raise ValueError
|
|
||||||
return self.xmpp.plugin['xep_0004'].buildForm(form)
|
|
||||||
|
|
||||||
def cancelConfig(self, room):
|
def getRoomConfig(self, room, ifrom=''):
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
|
||||||
x = ET.Element('{jabber:x:data}x', type='cancel')
|
iq['to'] = room
|
||||||
query.append(x)
|
iq['from'] = ifrom
|
||||||
iq = self.xmpp.makeIqSet(query)
|
result = iq.send()
|
||||||
iq.send()
|
if result is None or result['type'] != 'result':
|
||||||
|
raise ValueError
|
||||||
|
form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
||||||
|
if form is None:
|
||||||
|
raise ValueError
|
||||||
|
return self.xmpp.plugin['xep_0004'].buildForm(form)
|
||||||
|
|
||||||
def setRoomConfig(self, room, config):
|
def cancelConfig(self, room):
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
x = config.getXML('submit')
|
x = ET.Element('{jabber:x:data}x', type='cancel')
|
||||||
query.append(x)
|
query.append(x)
|
||||||
iq = self.xmpp.makeIqSet(query)
|
iq = self.xmpp.makeIqSet(query)
|
||||||
iq['to'] = room
|
iq['to'] = room
|
||||||
iq['from'] = self.xmpp.jid
|
iq.send()
|
||||||
iq.send()
|
|
||||||
|
|
||||||
def getJoinedRooms(self):
|
def setRoomConfig(self, room, config, ifrom=''):
|
||||||
return self.rooms.keys()
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
|
x = config.getXML('submit')
|
||||||
|
query.append(x)
|
||||||
|
iq = self.xmpp.makeIqSet(query)
|
||||||
|
iq['to'] = room
|
||||||
|
iq['from'] = ifrom
|
||||||
|
iq.send()
|
||||||
|
|
||||||
def getOurJidInRoom(self, roomJid):
|
def getJoinedRooms(self):
|
||||||
""" Return the jid we're using in a room.
|
return self.rooms.keys()
|
||||||
"""
|
|
||||||
return "%s/%s" % (roomJid, self.ourNicks[roomJid])
|
|
||||||
|
|
||||||
def getJidProperty(self, room, nick, jidProperty):
|
def getOurJidInRoom(self, roomJid):
|
||||||
""" Get the property of a nick in a room, such as its 'jid' or 'affiliation'
|
""" Return the jid we're using in a room.
|
||||||
If not found, return None.
|
"""
|
||||||
"""
|
return "%s/%s" % (roomJid, self.ourNicks[roomJid])
|
||||||
if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
|
|
||||||
return self.rooms[room][nick][jidProperty]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getRoster(self, room):
|
def getJidProperty(self, room, nick, jidProperty):
|
||||||
""" Get the list of nicks in a room.
|
""" Get the property of a nick in a room, such as its 'jid' or 'affiliation'
|
||||||
"""
|
If not found, return None.
|
||||||
if room not in self.rooms.keys():
|
"""
|
||||||
return None
|
if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
|
||||||
return self.rooms[room].keys()
|
return self.rooms[room][nick][jidProperty]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getRoster(self, room):
|
||||||
|
""" Get the list of nicks in a room.
|
||||||
|
"""
|
||||||
|
if room not in self.rooms.keys():
|
||||||
|
return None
|
||||||
|
return self.rooms[room].keys()
|
||||||
|
|||||||
10
sleekxmpp/plugins/xep_0050/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0050/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0050.stanza import Command
|
||||||
|
from sleekxmpp.plugins.xep_0050.adhoc import xep_0050
|
||||||
593
sleekxmpp/plugins/xep_0050/adhoc.py
Normal file
593
sleekxmpp/plugins/xep_0050/adhoc.py
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin, JID
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0050 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0050 import Command
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0050(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0050: Ad-Hoc Commands
|
||||||
|
|
||||||
|
XMPP's Adhoc Commands provides a generic workflow mechanism for
|
||||||
|
interacting with applications. The result is similar to menu selections
|
||||||
|
and multi-step dialogs in normal desktop applications. Clients do not
|
||||||
|
need to know in advance what commands are provided by any particular
|
||||||
|
application or agent. While adhoc commands provide similar functionality
|
||||||
|
to Jabber-RPC, adhoc commands are used primarily for human interaction.
|
||||||
|
|
||||||
|
Also see <http://xmpp.org/extensions/xep-0050.html>
|
||||||
|
|
||||||
|
Configuration Values:
|
||||||
|
threaded -- Indicates if command events should be threaded.
|
||||||
|
Defaults to True.
|
||||||
|
|
||||||
|
Events:
|
||||||
|
command_execute -- Received a command with action="execute"
|
||||||
|
command_next -- Received a command with action="next"
|
||||||
|
command_complete -- Received a command with action="complete"
|
||||||
|
command_cancel -- Received a command with action="cancel"
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
threaded -- Indicates if command events should be threaded.
|
||||||
|
Defaults to True.
|
||||||
|
commands -- A dictionary mapping JID/node pairs to command
|
||||||
|
names and handlers.
|
||||||
|
sessions -- A dictionary or equivalent backend mapping
|
||||||
|
session IDs to dictionaries containing data
|
||||||
|
relevant to a command's session.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
plugin_init -- Overrides base_plugin.plugin_init
|
||||||
|
post_init -- Overrides base_plugin.post_init
|
||||||
|
new_session -- Return a new session ID.
|
||||||
|
prep_handlers -- Placeholder. May call with a list of handlers
|
||||||
|
to prepare them for use with the session storage
|
||||||
|
backend, if needed.
|
||||||
|
set_backend -- Replace the default session storage with some
|
||||||
|
external storage mechanism, such as a database.
|
||||||
|
The provided backend wrapper must be able to
|
||||||
|
act using the same syntax as a dictionary.
|
||||||
|
add_command -- Add a command for use by external entitites.
|
||||||
|
get_commands -- Retrieve a list of commands provided by a
|
||||||
|
remote agent.
|
||||||
|
send_command -- Send a command request to a remote agent.
|
||||||
|
start_command -- Command user API: initiate a command session
|
||||||
|
continue_command -- Command user API: proceed to the next step
|
||||||
|
cancel_command -- Command user API: cancel a command
|
||||||
|
complete_command -- Command user API: finish a command
|
||||||
|
terminate_command -- Command user API: delete a command's session
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""Start the XEP-0050 plugin."""
|
||||||
|
self.xep = '0050'
|
||||||
|
self.description = 'Ad-Hoc Commands'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
self.threaded = self.config.get('threaded', True)
|
||||||
|
self.commands = {}
|
||||||
|
self.sessions = self.config.get('session_db', {})
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback("Ad-Hoc Execute",
|
||||||
|
StanzaPath('iq@type=set/command'),
|
||||||
|
self._handle_command))
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback("Ad-Hoc Result",
|
||||||
|
StanzaPath('iq@type=result/command'),
|
||||||
|
self._handle_command_result))
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback("Ad-Hoc Error",
|
||||||
|
StanzaPath('iq@type=error/command'),
|
||||||
|
self._handle_command_result))
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, stanza.Command)
|
||||||
|
|
||||||
|
self.xmpp.add_event_handler('command_execute',
|
||||||
|
self._handle_command_start,
|
||||||
|
threaded=self.threaded)
|
||||||
|
self.xmpp.add_event_handler('command_next',
|
||||||
|
self._handle_command_next,
|
||||||
|
threaded=self.threaded)
|
||||||
|
self.xmpp.add_event_handler('command_cancel',
|
||||||
|
self._handle_command_cancel,
|
||||||
|
threaded=self.threaded)
|
||||||
|
self.xmpp.add_event_handler('command_complete',
|
||||||
|
self._handle_command_complete,
|
||||||
|
threaded=self.threaded)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Handle cross-plugin interactions."""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp['xep_0030'].add_feature(Command.namespace)
|
||||||
|
|
||||||
|
def set_backend(self, db):
|
||||||
|
"""
|
||||||
|
Replace the default session storage dictionary with
|
||||||
|
a generic, external data storage mechanism.
|
||||||
|
|
||||||
|
The replacement backend must be able to interact through
|
||||||
|
the same syntax and interfaces as a normal dictionary.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
db -- The new session storage mechanism.
|
||||||
|
"""
|
||||||
|
self.sessions = db
|
||||||
|
|
||||||
|
def prep_handlers(self, handlers, **kwargs):
|
||||||
|
"""
|
||||||
|
Prepare a list of functions for use by the backend service.
|
||||||
|
|
||||||
|
Intended to be replaced by the backend service as needed.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
handlers -- A list of function pointers
|
||||||
|
**kwargs -- Any additional parameters required by the backend.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Server side (command provider) API
|
||||||
|
|
||||||
|
def add_command(self, jid=None, node=None, name='', handler=None):
|
||||||
|
"""
|
||||||
|
Make a new command available to external entities.
|
||||||
|
|
||||||
|
Access control may be implemented in the provided handler.
|
||||||
|
|
||||||
|
Command workflow is done across a sequence of command handlers. The
|
||||||
|
first handler is given the intial Iq stanza of the request in order
|
||||||
|
to support access control. Subsequent handlers are given only the
|
||||||
|
payload items of the command. All handlers will receive the command's
|
||||||
|
session data.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID that will expose the command.
|
||||||
|
node -- The node associated with the command.
|
||||||
|
name -- A human readable name for the command.
|
||||||
|
handler -- A function that will generate the response to the
|
||||||
|
initial command request, as well as enforcing any
|
||||||
|
access control policies.
|
||||||
|
"""
|
||||||
|
if jid is None:
|
||||||
|
jid = self.xmpp.boundjid
|
||||||
|
elif not isinstance(jid, JID):
|
||||||
|
jid = JID(jid)
|
||||||
|
item_jid = jid.full
|
||||||
|
|
||||||
|
# Client disco uses only the bare JID
|
||||||
|
if self.xmpp.is_component:
|
||||||
|
jid = jid.full
|
||||||
|
else:
|
||||||
|
jid = jid.bare
|
||||||
|
|
||||||
|
self.xmpp['xep_0030'].add_identity(category='automation',
|
||||||
|
itype='command-list',
|
||||||
|
name='Ad-Hoc commands',
|
||||||
|
node=Command.namespace,
|
||||||
|
jid=jid)
|
||||||
|
self.xmpp['xep_0030'].add_item(jid=item_jid,
|
||||||
|
name=name,
|
||||||
|
node=Command.namespace,
|
||||||
|
subnode=node,
|
||||||
|
ijid=jid)
|
||||||
|
self.xmpp['xep_0030'].add_identity(category='automation',
|
||||||
|
itype='command-node',
|
||||||
|
name=name,
|
||||||
|
node=node,
|
||||||
|
jid=jid)
|
||||||
|
self.xmpp['xep_0030'].add_feature(Command.namespace, None, jid)
|
||||||
|
|
||||||
|
self.commands[(item_jid, node)] = (name, handler)
|
||||||
|
|
||||||
|
def new_session(self):
|
||||||
|
"""Return a new session ID."""
|
||||||
|
return str(time.time()) + '-' + self.xmpp.new_id()
|
||||||
|
|
||||||
|
def _handle_command(self, iq):
|
||||||
|
"""Raise command events based on the command action."""
|
||||||
|
self.xmpp.event('command_%s' % iq['command']['action'], iq)
|
||||||
|
|
||||||
|
def _handle_command_start(self, iq):
|
||||||
|
"""
|
||||||
|
Process an initial request to execute a command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command execution request.
|
||||||
|
"""
|
||||||
|
sessionid = self.new_session()
|
||||||
|
node = iq['command']['node']
|
||||||
|
key = (iq['to'].full, node)
|
||||||
|
name, handler = self.commands.get(key, ('Not found', None))
|
||||||
|
if not handler:
|
||||||
|
log.debug('Command not found: %s, %s' % (key, self.commands))
|
||||||
|
|
||||||
|
initial_session = {'id': sessionid,
|
||||||
|
'from': iq['from'],
|
||||||
|
'to': iq['to'],
|
||||||
|
'node': node,
|
||||||
|
'payload': None,
|
||||||
|
'interfaces': '',
|
||||||
|
'payload_classes': None,
|
||||||
|
'notes': None,
|
||||||
|
'has_next': False,
|
||||||
|
'allow_complete': False,
|
||||||
|
'allow_prev': False,
|
||||||
|
'past': [],
|
||||||
|
'next': None,
|
||||||
|
'prev': None,
|
||||||
|
'cancel': None}
|
||||||
|
|
||||||
|
session = handler(iq, initial_session)
|
||||||
|
|
||||||
|
self._process_command_response(iq, session)
|
||||||
|
|
||||||
|
def _handle_command_next(self, iq):
|
||||||
|
"""
|
||||||
|
Process a request for the next step in the workflow
|
||||||
|
for a command with multiple steps.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command continuation request.
|
||||||
|
"""
|
||||||
|
sessionid = iq['command']['sessionid']
|
||||||
|
session = self.sessions[sessionid]
|
||||||
|
|
||||||
|
handler = session['next']
|
||||||
|
interfaces = session['interfaces']
|
||||||
|
results = []
|
||||||
|
for stanza in iq['command']['substanzas']:
|
||||||
|
if stanza.plugin_attrib in interfaces:
|
||||||
|
results.append(stanza)
|
||||||
|
if len(results) == 1:
|
||||||
|
results = results[0]
|
||||||
|
|
||||||
|
session = handler(results, session)
|
||||||
|
|
||||||
|
self._process_command_response(iq, session)
|
||||||
|
|
||||||
|
def _process_command_response(self, iq, session):
|
||||||
|
"""
|
||||||
|
Generate a command reply stanza based on the
|
||||||
|
provided session data.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command request stanza.
|
||||||
|
session -- A dictionary of relevant session data.
|
||||||
|
"""
|
||||||
|
sessionid = session['id']
|
||||||
|
|
||||||
|
payload = session['payload']
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
payload = [payload]
|
||||||
|
|
||||||
|
session['interfaces'] = [item.plugin_attrib for item in payload]
|
||||||
|
session['payload_classes'] = [item.__class__ for item in payload]
|
||||||
|
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
for item in payload:
|
||||||
|
register_stanza_plugin(Command, item.__class__, iterable=True)
|
||||||
|
|
||||||
|
iq.reply()
|
||||||
|
iq['command']['node'] = session['node']
|
||||||
|
iq['command']['sessionid'] = session['id']
|
||||||
|
|
||||||
|
if session['next'] is None:
|
||||||
|
iq['command']['actions'] = []
|
||||||
|
iq['command']['status'] = 'completed'
|
||||||
|
elif session['has_next']:
|
||||||
|
actions = ['next']
|
||||||
|
if session['allow_complete']:
|
||||||
|
actions.append('complete')
|
||||||
|
if session['allow_prev']:
|
||||||
|
actions.append('prev')
|
||||||
|
iq['command']['actions'] = actions
|
||||||
|
iq['command']['status'] = 'executing'
|
||||||
|
else:
|
||||||
|
iq['command']['actions'] = ['complete']
|
||||||
|
iq['command']['status'] = 'executing'
|
||||||
|
|
||||||
|
iq['command']['notes'] = session['notes']
|
||||||
|
|
||||||
|
for item in payload:
|
||||||
|
iq['command'].append(item)
|
||||||
|
|
||||||
|
iq.send()
|
||||||
|
|
||||||
|
def _handle_command_cancel(self, iq):
|
||||||
|
"""
|
||||||
|
Process a request to cancel a command's execution.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command cancellation request.
|
||||||
|
"""
|
||||||
|
node = iq['command']['node']
|
||||||
|
sessionid = iq['command']['sessionid']
|
||||||
|
session = self.sessions[sessionid]
|
||||||
|
handler = session['cancel']
|
||||||
|
|
||||||
|
if handler:
|
||||||
|
handler(iq, session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
del self.sessions[sessionid]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
iq.reply()
|
||||||
|
iq['command']['node'] = node
|
||||||
|
iq['command']['sessionid'] = sessionid
|
||||||
|
iq['command']['status'] = 'canceled'
|
||||||
|
iq['command']['notes'] = session['notes']
|
||||||
|
iq.send()
|
||||||
|
|
||||||
|
def _handle_command_complete(self, iq):
|
||||||
|
"""
|
||||||
|
Process a request to finish the execution of command
|
||||||
|
and terminate the workflow.
|
||||||
|
|
||||||
|
All data related to the command session will be removed.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command completion request.
|
||||||
|
"""
|
||||||
|
node = iq['command']['node']
|
||||||
|
sessionid = iq['command']['sessionid']
|
||||||
|
session = self.sessions[sessionid]
|
||||||
|
handler = session['next']
|
||||||
|
interfaces = session['interfaces']
|
||||||
|
results = []
|
||||||
|
for stanza in iq['command']['substanzas']:
|
||||||
|
if stanza.plugin_attrib in interfaces:
|
||||||
|
results.append(stanza)
|
||||||
|
if len(results) == 1:
|
||||||
|
results = results[0]
|
||||||
|
|
||||||
|
if handler:
|
||||||
|
handler(results, session)
|
||||||
|
|
||||||
|
iq.reply()
|
||||||
|
iq['command']['node'] = node
|
||||||
|
iq['command']['sessionid'] = sessionid
|
||||||
|
iq['command']['actions'] = []
|
||||||
|
iq['command']['status'] = 'completed'
|
||||||
|
iq['command']['notes'] = session['notes']
|
||||||
|
iq.send()
|
||||||
|
|
||||||
|
del self.sessions[sessionid]
|
||||||
|
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Client side (command user) API
|
||||||
|
|
||||||
|
def get_commands(self, jid, **kwargs):
|
||||||
|
"""
|
||||||
|
Return a list of commands provided by a given JID.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to query for commands.
|
||||||
|
local -- If true, then the query is for a JID/node
|
||||||
|
combination handled by this Sleek instance and
|
||||||
|
no stanzas need to be sent.
|
||||||
|
Otherwise, a disco stanza must be sent to the
|
||||||
|
remove JID to retrieve the items.
|
||||||
|
ifrom -- Specifiy the sender's JID.
|
||||||
|
block -- If true, block and wait for the stanzas' reply.
|
||||||
|
timeout -- The time in seconds to block while waiting for
|
||||||
|
a reply. If None, then wait indefinitely.
|
||||||
|
callback -- Optional callback to execute when a reply is
|
||||||
|
received instead of blocking and waiting for
|
||||||
|
the reply.
|
||||||
|
iterator -- If True, return a result set iterator using
|
||||||
|
the XEP-0059 plugin, if the plugin is loaded.
|
||||||
|
Otherwise the parameter is ignored.
|
||||||
|
"""
|
||||||
|
return self.xmpp['xep_0030'].get_items(jid=jid,
|
||||||
|
node=Command.namespace,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def send_command(self, jid, node, ifrom=None, action='execute',
|
||||||
|
payload=None, sessionid=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Create and send a command stanza, without using the provided
|
||||||
|
workflow management APIs.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to send the command request or result.
|
||||||
|
node -- The node for the command.
|
||||||
|
ifrom -- Specify the sender's JID.
|
||||||
|
action -- May be one of: execute, cancel, complete,
|
||||||
|
or cancel.
|
||||||
|
payload -- Either a list of payload items, or a single
|
||||||
|
payload item such as a data form.
|
||||||
|
sessionid -- The current session's ID value.
|
||||||
|
block -- Specify if the send call will block until a
|
||||||
|
response is received, or a timeout occurs.
|
||||||
|
Defaults to True.
|
||||||
|
timeout -- The length of time (in seconds) to wait for a
|
||||||
|
response before exiting the send call
|
||||||
|
if blocking is used. Defaults to
|
||||||
|
sleekxmpp.xmlstream.RESPONSE_TIMEOUT
|
||||||
|
callback -- Optional reference to a stream handler
|
||||||
|
function. Will be executed when a reply
|
||||||
|
stanza is received.
|
||||||
|
"""
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['to'] = jid
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
iq['command']['node'] = node
|
||||||
|
iq['command']['action'] = action
|
||||||
|
if sessionid is not None:
|
||||||
|
iq['command']['sessionid'] = sessionid
|
||||||
|
if payload is not None:
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
payload = [payload]
|
||||||
|
for item in payload:
|
||||||
|
iq['command'].append(item)
|
||||||
|
return iq.send(**kwargs)
|
||||||
|
|
||||||
|
def start_command(self, jid, node, session, ifrom=None):
|
||||||
|
"""
|
||||||
|
Initiate executing a command provided by a remote agent.
|
||||||
|
|
||||||
|
The workflow provided is always non-blocking.
|
||||||
|
|
||||||
|
The provided session dictionary should contain:
|
||||||
|
next -- A handler for processing the command result.
|
||||||
|
error -- A handler for processing any error stanzas
|
||||||
|
generated by the request.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to send the command request.
|
||||||
|
node -- The node for the desired command.
|
||||||
|
session -- A dictionary of relevant session data.
|
||||||
|
ifrom -- Optionally specify the sender's JID.
|
||||||
|
"""
|
||||||
|
session['jid'] = jid
|
||||||
|
session['node'] = node
|
||||||
|
session['timestamp'] = time.time()
|
||||||
|
session['payload'] = None
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['to'] = jid
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
session['from'] = ifrom
|
||||||
|
iq['command']['node'] = node
|
||||||
|
iq['command']['action'] = 'execute'
|
||||||
|
sessionid = 'client:pending_' + iq['id']
|
||||||
|
session['id'] = sessionid
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
iq.send(block=False)
|
||||||
|
|
||||||
|
def continue_command(self, session):
|
||||||
|
"""
|
||||||
|
Execute the next action of the command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
session -- All stored data relevant to the current
|
||||||
|
command session.
|
||||||
|
"""
|
||||||
|
sessionid = 'client:' + session['id']
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
self.send_command(session['jid'],
|
||||||
|
session['node'],
|
||||||
|
ifrom=session.get('from', None),
|
||||||
|
action='next',
|
||||||
|
payload=session.get('payload', None),
|
||||||
|
sessionid=session['id'])
|
||||||
|
|
||||||
|
def cancel_command(self, session):
|
||||||
|
"""
|
||||||
|
Cancel the execution of a command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
session -- All stored data relevant to the current
|
||||||
|
command session.
|
||||||
|
"""
|
||||||
|
sessionid = 'client:' + session['id']
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
self.send_command(session['jid'],
|
||||||
|
session['node'],
|
||||||
|
ifrom=session.get('from', None),
|
||||||
|
action='cancel',
|
||||||
|
payload=session.get('payload', None),
|
||||||
|
sessionid=session['id'])
|
||||||
|
|
||||||
|
def complete_command(self, session):
|
||||||
|
"""
|
||||||
|
Finish the execution of a command workflow.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
session -- All stored data relevant to the current
|
||||||
|
command session.
|
||||||
|
"""
|
||||||
|
sessionid = 'client:' + session['id']
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
self.send_command(session['jid'],
|
||||||
|
session['node'],
|
||||||
|
ifrom=session.get('from', None),
|
||||||
|
action='complete',
|
||||||
|
payload=session.get('payload', None),
|
||||||
|
sessionid=session['id'])
|
||||||
|
|
||||||
|
def terminate_command(self, session):
|
||||||
|
"""
|
||||||
|
Delete a command's session after a command has completed
|
||||||
|
or an error has occured.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
session -- All stored data relevant to the current
|
||||||
|
command session.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
del self.sessions[session['id']]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _handle_command_result(self, iq):
|
||||||
|
"""
|
||||||
|
Process the results of a command request.
|
||||||
|
|
||||||
|
Will execute the 'next' handler stored in the session
|
||||||
|
data, or the 'error' handler depending on the Iq's type.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command response.
|
||||||
|
"""
|
||||||
|
sessionid = 'client:' + iq['command']['sessionid']
|
||||||
|
pending = False
|
||||||
|
|
||||||
|
if sessionid not in self.sessions:
|
||||||
|
pending = True
|
||||||
|
pendingid = 'client:pending_' + iq['id']
|
||||||
|
if pendingid not in self.sessions:
|
||||||
|
return
|
||||||
|
sessionid = pendingid
|
||||||
|
|
||||||
|
session = self.sessions[sessionid]
|
||||||
|
sessionid = 'client:' + iq['command']['sessionid']
|
||||||
|
session['id'] = iq['command']['sessionid']
|
||||||
|
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
if pending:
|
||||||
|
del self.sessions[pendingid]
|
||||||
|
|
||||||
|
handler_type = 'next'
|
||||||
|
if iq['type'] == 'error':
|
||||||
|
handler_type = 'error'
|
||||||
|
handler = session.get(handler_type, None)
|
||||||
|
if handler:
|
||||||
|
handler(iq, session)
|
||||||
|
elif iq['type'] == 'error':
|
||||||
|
self.terminate_command(session)
|
||||||
|
|
||||||
|
if iq['command']['status'] == 'completed':
|
||||||
|
self.terminate_command(session)
|
||||||
185
sleekxmpp/plugins/xep_0050/stanza.py
Normal file
185
sleekxmpp/plugins/xep_0050/stanza.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET
|
||||||
|
|
||||||
|
|
||||||
|
class Command(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XMPP's Adhoc Commands provides a generic workflow mechanism for
|
||||||
|
interacting with applications. The result is similar to menu selections
|
||||||
|
and multi-step dialogs in normal desktop applications. Clients do not
|
||||||
|
need to know in advance what commands are provided by any particular
|
||||||
|
application or agent. While adhoc commands provide similar functionality
|
||||||
|
to Jabber-RPC, adhoc commands are used primarily for human interaction.
|
||||||
|
|
||||||
|
Also see <http://xmpp.org/extensions/xep-0050.html>
|
||||||
|
|
||||||
|
Example command stanzas:
|
||||||
|
<iq type="set">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="run_foo"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
<iq type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="run_foo"
|
||||||
|
sessionid="12345"
|
||||||
|
status="executing">
|
||||||
|
<actions>
|
||||||
|
<complete />
|
||||||
|
</actions>
|
||||||
|
<note type="info">Information!</note>
|
||||||
|
<x xmlns="jabber:x:data">
|
||||||
|
<field var="greeting"
|
||||||
|
type="text-single"
|
||||||
|
label="Greeting" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
action -- The action to perform.
|
||||||
|
actions -- The set of allowable next actions.
|
||||||
|
node -- The node associated with the command.
|
||||||
|
notes -- A list of tuples for informative notes.
|
||||||
|
sessionid -- A unique identifier for a command session.
|
||||||
|
status -- May be one of: canceled, completed, or executing.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
actions -- A set of allowed action values.
|
||||||
|
statuses -- A set of allowed status values.
|
||||||
|
next_actions -- A set of allowed next action names.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
get_action -- Return the requested action.
|
||||||
|
get_actions -- Return the allowable next actions.
|
||||||
|
set_actions -- Set the allowable next actions.
|
||||||
|
del_actions -- Remove the current set of next actions.
|
||||||
|
get_notes -- Return a list of informative note data.
|
||||||
|
set_notes -- Set informative notes.
|
||||||
|
del_notes -- Remove any note data.
|
||||||
|
add_note -- Add a single note.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'command'
|
||||||
|
namespace = 'http://jabber.org/protocol/commands'
|
||||||
|
plugin_attrib = 'command'
|
||||||
|
interfaces = set(('action', 'sessionid', 'node',
|
||||||
|
'status', 'actions', 'notes'))
|
||||||
|
actions = set(('cancel', 'complete', 'execute', 'next', 'prev'))
|
||||||
|
statuses = set(('canceled', 'completed', 'executing'))
|
||||||
|
next_actions = set(('prev', 'next', 'complete'))
|
||||||
|
|
||||||
|
def get_action(self):
|
||||||
|
"""
|
||||||
|
Return the value of the action attribute.
|
||||||
|
|
||||||
|
If the Iq stanza's type is "set" then use a default
|
||||||
|
value of "execute".
|
||||||
|
"""
|
||||||
|
if self.parent()['type'] == 'set':
|
||||||
|
return self._get_attr('action', default='execute')
|
||||||
|
return self._get_attr('action')
|
||||||
|
|
||||||
|
def set_actions(self, values):
|
||||||
|
"""
|
||||||
|
Assign the set of allowable next actions.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
values -- A list containing any combination of:
|
||||||
|
'prev', 'next', and 'complete'
|
||||||
|
"""
|
||||||
|
self.del_actions()
|
||||||
|
if values:
|
||||||
|
self._set_sub_text('{%s}actions' % self.namespace, '', True)
|
||||||
|
actions = self.find('{%s}actions' % self.namespace)
|
||||||
|
for val in values:
|
||||||
|
if val in self.next_actions:
|
||||||
|
action = ET.Element('{%s}%s' % (self.namespace, val))
|
||||||
|
actions.append(action)
|
||||||
|
|
||||||
|
def get_actions(self):
|
||||||
|
"""
|
||||||
|
Return the set of allowable next actions.
|
||||||
|
"""
|
||||||
|
actions = []
|
||||||
|
actions_xml = self.find('{%s}actions' % self.namespace)
|
||||||
|
if actions_xml is not None:
|
||||||
|
for action in self.next_actions:
|
||||||
|
action_xml = actions_xml.find('{%s}%s' % (self.namespace,
|
||||||
|
action))
|
||||||
|
if action_xml is not None:
|
||||||
|
actions.append(action)
|
||||||
|
return actions
|
||||||
|
|
||||||
|
def del_actions(self):
|
||||||
|
"""
|
||||||
|
Remove all allowable next actions.
|
||||||
|
"""
|
||||||
|
self._del_sub('{%s}actions' % self.namespace)
|
||||||
|
|
||||||
|
def get_notes(self):
|
||||||
|
"""
|
||||||
|
Return a list of note information.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
[('info', 'Some informative data'),
|
||||||
|
('warning', 'Use caution'),
|
||||||
|
('error', 'The command ran, but had errors')]
|
||||||
|
"""
|
||||||
|
notes = []
|
||||||
|
notes_xml = self.findall('{%s}note' % self.namespace)
|
||||||
|
for note in notes_xml:
|
||||||
|
notes.append((note.attrib.get('type', 'info'),
|
||||||
|
note.text))
|
||||||
|
return notes
|
||||||
|
|
||||||
|
def set_notes(self, notes):
|
||||||
|
"""
|
||||||
|
Add multiple notes to the command result.
|
||||||
|
|
||||||
|
Each note is a tuple, with the first item being one of:
|
||||||
|
'info', 'warning', or 'error', and the second item being
|
||||||
|
any human readable message.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
[('info', 'Some informative data'),
|
||||||
|
('warning', 'Use caution'),
|
||||||
|
('error', 'The command ran, but had errors')]
|
||||||
|
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
notes -- A list of tuples of note information.
|
||||||
|
"""
|
||||||
|
self.del_notes()
|
||||||
|
for note in notes:
|
||||||
|
self.add_note(note[1], note[0])
|
||||||
|
|
||||||
|
def del_notes(self):
|
||||||
|
"""
|
||||||
|
Remove all notes associated with the command result.
|
||||||
|
"""
|
||||||
|
notes_xml = self.findall('{%s}note' % self.namespace)
|
||||||
|
for note in notes_xml:
|
||||||
|
self.xml.remove(note)
|
||||||
|
|
||||||
|
def add_note(self, msg='', ntype='info'):
|
||||||
|
"""
|
||||||
|
Add a single note annotation to the command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg -- A human readable message.
|
||||||
|
ntype -- One of: 'info', 'warning', 'error'
|
||||||
|
"""
|
||||||
|
xml = ET.Element('{%s}note' % self.namespace)
|
||||||
|
xml.attrib['type'] = ntype
|
||||||
|
xml.text = msg
|
||||||
|
self.xml.append(xml)
|
||||||
10
sleekxmpp/plugins/xep_0059/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0059/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0059.stanza import Set
|
||||||
|
from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, xep_0059
|
||||||
119
sleekxmpp/plugins/xep_0059/rsm.py
Normal file
119
sleekxmpp/plugins/xep_0059/rsm.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0059 import Set
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ResultIterator():
|
||||||
|
|
||||||
|
"""
|
||||||
|
An iterator for Result Set Managment
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, query, interface, amount=10, start=None, reverse=False):
|
||||||
|
"""
|
||||||
|
Arguments:
|
||||||
|
query -- The template query
|
||||||
|
interface -- The substanza of the query, for example disco_items
|
||||||
|
amount -- The max amounts of items to request per iteration
|
||||||
|
start -- From which item id to start
|
||||||
|
reverse -- If True, page backwards through the results
|
||||||
|
|
||||||
|
Example:
|
||||||
|
q = Iq()
|
||||||
|
q['to'] = 'pubsub.example.com'
|
||||||
|
q['disco_items']['node'] = 'blog'
|
||||||
|
for i in ResultIterator(q, 'disco_items', '10'):
|
||||||
|
print i['disco_items']['items']
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.query = query
|
||||||
|
self.amount = amount
|
||||||
|
self.start = start
|
||||||
|
self.interface = interface
|
||||||
|
self.reverse = reverse
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
return self.next()
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
"""
|
||||||
|
Return the next page of results from a query.
|
||||||
|
|
||||||
|
Note: If using backwards paging, then the next page of
|
||||||
|
results will be the items before the current page
|
||||||
|
of items.
|
||||||
|
"""
|
||||||
|
self.query[self.interface]['rsm']['before'] = self.reverse
|
||||||
|
self.query['id'] = self.query.stream.new_id()
|
||||||
|
self.query[self.interface]['rsm']['max'] = str(self.amount)
|
||||||
|
|
||||||
|
if self.start and self.reverse:
|
||||||
|
self.query[self.interface]['rsm']['before'] = self.start
|
||||||
|
elif self.start:
|
||||||
|
self.query[self.interface]['rsm']['after'] = self.start
|
||||||
|
|
||||||
|
r = self.query.send(block=True)
|
||||||
|
|
||||||
|
if not r or not r[self.interface]['rsm']['first'] and \
|
||||||
|
not r[self.interface]['rsm']['last']:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
if self.reverse:
|
||||||
|
self.start = r[self.interface]['rsm']['first']
|
||||||
|
else:
|
||||||
|
self.start = r[self.interface]['rsm']['last']
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0059(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0050: Result Set Management
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""
|
||||||
|
Start the XEP-0059 plugin.
|
||||||
|
"""
|
||||||
|
self.xep = '0059'
|
||||||
|
self.description = 'Result Set Management'
|
||||||
|
self.stanza = sleekxmpp.plugins.xep_0059.stanza
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Handle inter-plugin dependencies."""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp['xep_0030'].add_feature(Set.namespace)
|
||||||
|
|
||||||
|
def iterate(self, stanza, interface):
|
||||||
|
"""
|
||||||
|
Create a new result set iterator for a given stanza query.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
stanza -- A stanza object to serve as a template for
|
||||||
|
queries made each iteration. For example, a
|
||||||
|
basic disco#items query.
|
||||||
|
interface -- The name of the substanza to which the
|
||||||
|
result set management stanza should be
|
||||||
|
appended. For example, for disco#items queries
|
||||||
|
the interface 'disco_items' should be used.
|
||||||
|
"""
|
||||||
|
return ResultIterator(stanza, interface)
|
||||||
108
sleekxmpp/plugins/xep_0059/stanza.py
Normal file
108
sleekxmpp/plugins/xep_0059/stanza.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET
|
||||||
|
from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems
|
||||||
|
|
||||||
|
|
||||||
|
class Set(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0059 (Result Set Managment) can be used to manage the
|
||||||
|
results of queries. For example, limiting the number of items
|
||||||
|
per response or starting at certain positions.
|
||||||
|
|
||||||
|
Example set stanzas:
|
||||||
|
<iq type="get">
|
||||||
|
<query xmlns="http://jabber.org/protocol/disco#items">
|
||||||
|
<set xmlns="http://jabber.org/protocol/rsm">
|
||||||
|
<max>2</max>
|
||||||
|
</set>
|
||||||
|
</query>
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
<iq type="result">
|
||||||
|
<query xmlns="http://jabber.org/protocol/disco#items">
|
||||||
|
<item jid="conference.example.com" />
|
||||||
|
<item jid="pubsub.example.com" />
|
||||||
|
<set xmlns="http://jabber.org/protocol/rsm">
|
||||||
|
<first>conference.example.com</first>
|
||||||
|
<last>pubsub.example.com</last>
|
||||||
|
</set>
|
||||||
|
</query>
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
first_index -- The index attribute of <first>
|
||||||
|
after -- The id defining from which item to start
|
||||||
|
before -- The id defining from which item to
|
||||||
|
start when browsing backwards
|
||||||
|
max -- Max amount per response
|
||||||
|
first -- Id for the first item in the response
|
||||||
|
last -- Id for the last item in the response
|
||||||
|
index -- Used to set an index to start from
|
||||||
|
count -- The number of remote items available
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
set_first_index -- Sets the index attribute for <first> and
|
||||||
|
creates the element if it doesn't exist
|
||||||
|
get_first_index -- Returns the value of the index
|
||||||
|
attribute for <first>
|
||||||
|
del_first_index -- Removes the index attribute for <first>
|
||||||
|
but keeps the element
|
||||||
|
set_before -- Sets the value of <before>, if the value is True
|
||||||
|
then the element will be created without a value
|
||||||
|
get_before -- Returns the value of <before>, if it is
|
||||||
|
empty it will return True
|
||||||
|
|
||||||
|
"""
|
||||||
|
namespace = 'http://jabber.org/protocol/rsm'
|
||||||
|
name = 'set'
|
||||||
|
plugin_attrib = 'rsm'
|
||||||
|
sub_interfaces = set(('first', 'after', 'before', 'count',
|
||||||
|
'index', 'last', 'max'))
|
||||||
|
interfaces = set(('first_index', 'first', 'after', 'before',
|
||||||
|
'count', 'index', 'last', 'max'))
|
||||||
|
|
||||||
|
def set_first_index(self, val):
|
||||||
|
fi = self.find("{%s}first" % (self.namespace))
|
||||||
|
if fi is not None:
|
||||||
|
if val:
|
||||||
|
fi.attrib['index'] = val
|
||||||
|
else:
|
||||||
|
del fi.attrib['index']
|
||||||
|
elif val:
|
||||||
|
fi = ET.Element("{%s}first" % (self.namespace))
|
||||||
|
fi.attrib['index'] = val
|
||||||
|
self.xml.append(fi)
|
||||||
|
|
||||||
|
def get_first_index(self):
|
||||||
|
fi = self.find("{%s}first" % (self.namespace))
|
||||||
|
if fi is not None:
|
||||||
|
return fi.attrib.get('index', '')
|
||||||
|
|
||||||
|
def del_first_index(self):
|
||||||
|
fi = self.xml.find("{%s}first" % (self.namespace))
|
||||||
|
if fi is not None:
|
||||||
|
del fi.attrib['index']
|
||||||
|
|
||||||
|
def set_before(self, val):
|
||||||
|
b = self.xml.find("{%s}before" % (self.namespace))
|
||||||
|
if b is None and val == True:
|
||||||
|
self._set_sub_text('{%s}before' % self.namespace, '', True)
|
||||||
|
else:
|
||||||
|
self._set_sub_text('{%s}before' % self.namespace, val)
|
||||||
|
|
||||||
|
def get_before(self):
|
||||||
|
b = self.xml.find("{%s}before" % (self.namespace))
|
||||||
|
if b is not None and not b.text:
|
||||||
|
return True
|
||||||
|
elif b is not None:
|
||||||
|
return b.text
|
||||||
|
else:
|
||||||
|
return None
|
||||||
@@ -51,7 +51,7 @@ class xep_0060(base.base_plugin):
|
|||||||
pubsub.append(configure)
|
pubsub.append(configure)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is False or result is None or result['type'] == 'error': return False
|
if result is False or result is None or result['type'] == 'error': return False
|
||||||
@@ -63,15 +63,15 @@ class xep_0060(base.base_plugin):
|
|||||||
subscribe.attrib['node'] = node
|
subscribe.attrib['node'] = node
|
||||||
if subscribee is None:
|
if subscribee is None:
|
||||||
if bare:
|
if bare:
|
||||||
subscribe.attrib['jid'] = self.xmpp.jid
|
subscribe.attrib['jid'] = self.xmpp.boundjid.bare
|
||||||
else:
|
else:
|
||||||
subscribe.attrib['jid'] = self.xmpp.fulljid
|
subscribe.attrib['jid'] = self.xmpp.boundjid.full
|
||||||
else:
|
else:
|
||||||
subscribe.attrib['jid'] = subscribee
|
subscribe.attrib['jid'] = subscribee
|
||||||
pubsub.append(subscribe)
|
pubsub.append(subscribe)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is False or result is None or result['type'] == 'error': return False
|
if result is False or result is None or result['type'] == 'error': return False
|
||||||
@@ -83,15 +83,15 @@ class xep_0060(base.base_plugin):
|
|||||||
unsubscribe.attrib['node'] = node
|
unsubscribe.attrib['node'] = node
|
||||||
if subscribee is None:
|
if subscribee is None:
|
||||||
if bare:
|
if bare:
|
||||||
unsubscribe.attrib['jid'] = self.xmpp.jid
|
unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare
|
||||||
else:
|
else:
|
||||||
unsubscribe.attrib['jid'] = self.xmpp.fulljid
|
unsubscribe.attrib['jid'] = self.xmpp.boundjid.full
|
||||||
else:
|
else:
|
||||||
unsubscribe.attrib['jid'] = subscribee
|
unsubscribe.attrib['jid'] = subscribee
|
||||||
pubsub.append(unsubscribe)
|
pubsub.append(unsubscribe)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is False or result is None or result['type'] == 'error': return False
|
if result is False or result is None or result['type'] == 'error': return False
|
||||||
@@ -109,7 +109,7 @@ class xep_0060(base.base_plugin):
|
|||||||
iq = self.xmpp.makeIqGet()
|
iq = self.xmpp.makeIqGet()
|
||||||
iq.append(pubsub)
|
iq.append(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
|
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
@@ -133,7 +133,7 @@ class xep_0060(base.base_plugin):
|
|||||||
iq = self.xmpp.makeIqGet()
|
iq = self.xmpp.makeIqGet()
|
||||||
iq.append(pubsub)
|
iq.append(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result == False or result['type'] == 'error':
|
if result is None or result == False or result['type'] == 'error':
|
||||||
@@ -156,7 +156,7 @@ class xep_0060(base.base_plugin):
|
|||||||
iq = self.xmpp.makeIqGet()
|
iq = self.xmpp.makeIqGet()
|
||||||
iq.append(pubsub)
|
iq.append(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result == False or result['type'] == 'error':
|
if result is None or result == False or result['type'] == 'error':
|
||||||
@@ -179,7 +179,7 @@ class xep_0060(base.base_plugin):
|
|||||||
pubsub.append(delete)
|
pubsub.append(delete)
|
||||||
iq.append(pubsub)
|
iq.append(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is not None and result is not False and result['type'] != 'error':
|
if result is not None and result is not False and result['type'] != 'error':
|
||||||
return True
|
return True
|
||||||
@@ -196,7 +196,7 @@ class xep_0060(base.base_plugin):
|
|||||||
pubsub.append(configure)
|
pubsub.append(configure)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result['type'] == 'error':
|
if result is None or result['type'] == 'error':
|
||||||
@@ -217,7 +217,7 @@ class xep_0060(base.base_plugin):
|
|||||||
pubsub.append(publish)
|
pubsub.append(publish)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result is False or result['type'] == 'error': return False
|
if result is None or result is False or result['type'] == 'error': return False
|
||||||
@@ -236,7 +236,7 @@ class xep_0060(base.base_plugin):
|
|||||||
pubsub.append(retract)
|
pubsub.append(retract)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result is False or result['type'] == 'error': return False
|
if result is None or result is False or result['type'] == 'error': return False
|
||||||
@@ -287,7 +287,7 @@ class xep_0060(base.base_plugin):
|
|||||||
pubsub.append(affs)
|
pubsub.append(affs)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = ps_jid
|
iq.attrib['to'] = ps_jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result is False or result['type'] == 'error':
|
if result is None or result is False or result['type'] == 'error':
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class xep_0078(base.base_plugin):
|
|||||||
log.debug("Starting jabber:iq:auth Authentication")
|
log.debug("Starting jabber:iq:auth Authentication")
|
||||||
auth_request = self.xmpp.makeIqGet()
|
auth_request = self.xmpp.makeIqGet()
|
||||||
auth_request_query = ET.Element('{jabber:iq:auth}query')
|
auth_request_query = ET.Element('{jabber:iq:auth}query')
|
||||||
auth_request.attrib['to'] = self.xmpp.server
|
auth_request.attrib['to'] = self.xmpp.boundjid.host
|
||||||
username = ET.Element('username')
|
username = ET.Element('username')
|
||||||
username.text = self.xmpp.username
|
username.text = self.xmpp.username
|
||||||
auth_request_query.append(username)
|
auth_request_query.append(username)
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
"""
|
|
||||||
SleekXMPP: The Sleek XMPP Library
|
|
||||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
|
||||||
This file is part of SleekXMPP.
|
|
||||||
|
|
||||||
See the file LICENSE for copying permissio
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from . import base
|
|
||||||
from .. xmlstream.handler.callback import Callback
|
|
||||||
from .. xmlstream.matcher.xpath import MatchXPath
|
|
||||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
|
||||||
from .. stanza.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ChatState(ElementBase):
|
|
||||||
namespace = 'http://jabber.org/protocol/chatstates'
|
|
||||||
plugin_attrib = 'chat_state'
|
|
||||||
interface = set(('state',))
|
|
||||||
states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
|
|
||||||
|
|
||||||
def active(self):
|
|
||||||
self.setState('active')
|
|
||||||
|
|
||||||
def composing(self):
|
|
||||||
self.setState('composing')
|
|
||||||
|
|
||||||
def gone(self):
|
|
||||||
self.setState('gone')
|
|
||||||
|
|
||||||
def inactive(self):
|
|
||||||
self.setState('inactive')
|
|
||||||
|
|
||||||
def paused(self):
|
|
||||||
self.setState('paused')
|
|
||||||
|
|
||||||
def setState(self, state):
|
|
||||||
if state in self.states:
|
|
||||||
self.name = state
|
|
||||||
self.xml.tag = '{%s}%s' % (self.namespace, state)
|
|
||||||
else:
|
|
||||||
raise ValueError('Invalid chat state')
|
|
||||||
|
|
||||||
def getState(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
# In order to match the various chat state elements,
|
|
||||||
# we need one stanza object per state, even though
|
|
||||||
# they are all the same except for the initial name
|
|
||||||
# value. Do not depend on the type of the chat state
|
|
||||||
# stanza object for the actual state.
|
|
||||||
|
|
||||||
class Active(ChatState):
|
|
||||||
name = 'active'
|
|
||||||
class Composing(ChatState):
|
|
||||||
name = 'composing'
|
|
||||||
class Gone(ChatState):
|
|
||||||
name = 'gone'
|
|
||||||
class Inactive(ChatState):
|
|
||||||
name = 'inactive'
|
|
||||||
class Paused(ChatState):
|
|
||||||
name = 'paused'
|
|
||||||
|
|
||||||
|
|
||||||
class xep_0085(base.base_plugin):
|
|
||||||
"""
|
|
||||||
XEP-0085 Chat State Notifications
|
|
||||||
"""
|
|
||||||
|
|
||||||
def plugin_init(self):
|
|
||||||
self.xep = '0085'
|
|
||||||
self.description = 'Chat State Notifications'
|
|
||||||
|
|
||||||
handlers = [('Active Chat State', 'active'),
|
|
||||||
('Composing Chat State', 'composing'),
|
|
||||||
('Gone Chat State', 'gone'),
|
|
||||||
('Inactive Chat State', 'inactive'),
|
|
||||||
('Paused Chat State', 'paused')]
|
|
||||||
for handler in handlers:
|
|
||||||
self.xmpp.registerHandler(
|
|
||||||
Callback(handler[0],
|
|
||||||
MatchXPath("{%s}message/{%s}%s" % (self.xmpp.default_ns,
|
|
||||||
ChatState.namespace,
|
|
||||||
handler[1])),
|
|
||||||
self._handleChatState))
|
|
||||||
|
|
||||||
registerStanzaPlugin(Message, Active)
|
|
||||||
registerStanzaPlugin(Message, Composing)
|
|
||||||
registerStanzaPlugin(Message, Gone)
|
|
||||||
registerStanzaPlugin(Message, Inactive)
|
|
||||||
registerStanzaPlugin(Message, Paused)
|
|
||||||
|
|
||||||
def post_init(self):
|
|
||||||
base.base_plugin.post_init(self)
|
|
||||||
self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/chatstates')
|
|
||||||
|
|
||||||
def _handleChatState(self, msg):
|
|
||||||
state = msg['chat_state'].name
|
|
||||||
log.debug("Chat State: %s, %s" % (state, msg['from'].jid))
|
|
||||||
self.xmpp.event('chatstate_%s' % state, msg)
|
|
||||||
10
sleekxmpp/plugins/xep_0085/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0085/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permissio
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0085.stanza import ChatState
|
||||||
|
from sleekxmpp.plugins.xep_0085.chat_states import xep_0085
|
||||||
49
sleekxmpp/plugins/xep_0085/chat_states.py
Normal file
49
sleekxmpp/plugins/xep_0085/chat_states.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permissio
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp.stanza import Message
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0085 import stanza, ChatState
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0085(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0085 Chat State Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = '0085'
|
||||||
|
self.description = 'Chat State Notifications'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
for state in ChatState.states:
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('Chat State: %s' % state,
|
||||||
|
StanzaPath('message@chat_state=%s' % state),
|
||||||
|
self._handle_chat_state))
|
||||||
|
|
||||||
|
register_stanza_plugin(Message, ChatState)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace)
|
||||||
|
|
||||||
|
def _handle_chat_state(self, msg):
|
||||||
|
state = msg['chat_state']
|
||||||
|
log.debug("Chat State: %s, %s" % (state, msg['from'].jid))
|
||||||
|
self.xmpp.event('chatstate_%s' % state, msg)
|
||||||
73
sleekxmpp/plugins/xep_0085/stanza.py
Normal file
73
sleekxmpp/plugins/xep_0085/stanza.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permissio
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET
|
||||||
|
|
||||||
|
|
||||||
|
class ChatState(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Example chat state stanzas:
|
||||||
|
<message>
|
||||||
|
<active xmlns="http://jabber.org/protocol/chatstates" />
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message>
|
||||||
|
<paused xmlns="http://jabber.org/protocol/chatstates" />
|
||||||
|
</message>
|
||||||
|
|
||||||
|
Stanza Interfaces:
|
||||||
|
chat_state
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
states
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
get_chat_state
|
||||||
|
set_chat_state
|
||||||
|
del_chat_state
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = ''
|
||||||
|
namespace = 'http://jabber.org/protocol/chatstates'
|
||||||
|
plugin_attrib = 'chat_state'
|
||||||
|
interfaces = set(('chat_state',))
|
||||||
|
is_extension = True
|
||||||
|
|
||||||
|
states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
|
||||||
|
|
||||||
|
def setup(self, xml=None):
|
||||||
|
self.xml = ET.Element('')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_chat_state(self):
|
||||||
|
parent = self.parent()
|
||||||
|
for state in self.states:
|
||||||
|
state_xml = parent.find('{%s}%s' % (self.namespace, state))
|
||||||
|
if state_xml is not None:
|
||||||
|
self.xml = state_xml
|
||||||
|
return state
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def set_chat_state(self, state):
|
||||||
|
self.del_chat_state()
|
||||||
|
parent = self.parent()
|
||||||
|
if state in self.states:
|
||||||
|
self.xml = ET.Element('{%s}%s' % (self.namespace, state))
|
||||||
|
parent.append(self.xml)
|
||||||
|
elif state not in [None, '']:
|
||||||
|
raise ValueError('Invalid chat state')
|
||||||
|
|
||||||
|
def del_chat_state(self):
|
||||||
|
parent = self.parent()
|
||||||
|
for state in self.states:
|
||||||
|
state_xml = parent.find('{%s}%s' % (self.namespace, state))
|
||||||
|
if state_xml is not None:
|
||||||
|
self.xml = ET.Element('')
|
||||||
|
parent.xml.remove(state_xml)
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
from . import base
|
|
||||||
import logging
|
|
||||||
from xml.etree import cElementTree as ET
|
|
||||||
import copy
|
|
||||||
|
|
||||||
class xep_0086(base.base_plugin):
|
|
||||||
"""
|
|
||||||
XEP-0086 Error Condition Mappings
|
|
||||||
"""
|
|
||||||
|
|
||||||
def plugin_init(self):
|
|
||||||
self.xep = '0086'
|
|
||||||
self.description = 'Error Condition Mappings'
|
|
||||||
self.error_map = {
|
|
||||||
'bad-request':('modify','400'),
|
|
||||||
'conflict':('cancel','409'),
|
|
||||||
'feature-not-implemented':('cancel','501'),
|
|
||||||
'forbidden':('auth','403'),
|
|
||||||
'gone':('modify','302'),
|
|
||||||
'internal-server-error':('wait','500'),
|
|
||||||
'item-not-found':('cancel','404'),
|
|
||||||
'jid-malformed':('modify','400'),
|
|
||||||
'not-acceptable':('modify','406'),
|
|
||||||
'not-allowed':('cancel','405'),
|
|
||||||
'not-authorized':('auth','401'),
|
|
||||||
'payment-required':('auth','402'),
|
|
||||||
'recipient-unavailable':('wait','404'),
|
|
||||||
'redirect':('modify','302'),
|
|
||||||
'registration-required':('auth','407'),
|
|
||||||
'remote-server-not-found':('cancel','404'),
|
|
||||||
'remote-server-timeout':('wait','504'),
|
|
||||||
'resource-constraint':('wait','500'),
|
|
||||||
'service-unavailable':('cancel','503'),
|
|
||||||
'subscription-required':('auth','407'),
|
|
||||||
'undefined-condition':(None,'500'),
|
|
||||||
'unexpected-request':('wait','400')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def makeError(self, condition, cdata=None, errorType=None, text=None, customElem=None):
|
|
||||||
conditionElem = self.xmpp.makeStanzaErrorCondition(condition, cdata)
|
|
||||||
if errorType is None:
|
|
||||||
error = self.xmpp.makeStanzaError(conditionElem, self.error_map[condition][0], self.error_map[condition][1], text, customElem)
|
|
||||||
else:
|
|
||||||
error = self.xmpp.makeStanzaError(conditionElem, errorType, self.error_map[condition][1], text, customElem)
|
|
||||||
error.append(conditionElem)
|
|
||||||
return error
|
|
||||||
10
sleekxmpp/plugins/xep_0086/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0086/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0086.stanza import LegacyError
|
||||||
|
from sleekxmpp.plugins.xep_0086.legacy_error import xep_0086
|
||||||
42
sleekxmpp/plugins/xep_0086/legacy_error.py
Normal file
42
sleekxmpp/plugins/xep_0086/legacy_error.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import Error
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0086 import stanza, LegacyError
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0086(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0086: Error Condition Mappings
|
||||||
|
|
||||||
|
Older XMPP implementations used code based error messages, similar
|
||||||
|
to HTTP response codes. Since then, error condition elements have
|
||||||
|
been introduced. XEP-0086 provides a mapping between the new
|
||||||
|
condition elements and a combination of error types and the older
|
||||||
|
response codes.
|
||||||
|
|
||||||
|
Also see <http://xmpp.org/extensions/xep-0086.html>.
|
||||||
|
|
||||||
|
Configuration Values:
|
||||||
|
override -- Indicates if applying legacy error codes should
|
||||||
|
be done automatically. Defaults to True.
|
||||||
|
If False, then inserting legacy error codes can
|
||||||
|
be done using:
|
||||||
|
iq['error']['legacy']['condition'] = ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = '0086'
|
||||||
|
self.description = 'Error Condition Mappings'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
register_stanza_plugin(Error, LegacyError,
|
||||||
|
overrides=self.config.get('override', True))
|
||||||
91
sleekxmpp/plugins/xep_0086/stanza.py
Normal file
91
sleekxmpp/plugins/xep_0086/stanza.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.stanza import Error
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyError(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Older XMPP implementations used code based error messages, similar
|
||||||
|
to HTTP response codes. Since then, error condition elements have
|
||||||
|
been introduced. XEP-0086 provides a mapping between the new
|
||||||
|
condition elements and a combination of error types and the older
|
||||||
|
response codes.
|
||||||
|
|
||||||
|
Also see <http://xmpp.org/extensions/xep-0086.html>.
|
||||||
|
|
||||||
|
Example legacy error stanzas:
|
||||||
|
<error xmlns="jabber:client" code="501" type="cancel">
|
||||||
|
<feature-not-implemented
|
||||||
|
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||||
|
</error>
|
||||||
|
|
||||||
|
<error code="402" type="auth">
|
||||||
|
<payment-required
|
||||||
|
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
|
||||||
|
</error>
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
error_map -- A map of error conditions to error types and
|
||||||
|
code values.
|
||||||
|
Methods:
|
||||||
|
setup -- Overrides ElementBase.setup
|
||||||
|
set_condition -- Remap the type and code interfaces when a
|
||||||
|
condition is set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'legacy'
|
||||||
|
namespace = Error.namespace
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('condition',))
|
||||||
|
overrides = ['set_condition']
|
||||||
|
|
||||||
|
error_map = {'bad-request': ('modify','400'),
|
||||||
|
'conflict': ('cancel','409'),
|
||||||
|
'feature-not-implemented': ('cancel','501'),
|
||||||
|
'forbidden': ('auth','403'),
|
||||||
|
'gone': ('modify','302'),
|
||||||
|
'internal-server-error': ('wait','500'),
|
||||||
|
'item-not-found': ('cancel','404'),
|
||||||
|
'jid-malformed': ('modify','400'),
|
||||||
|
'not-acceptable': ('modify','406'),
|
||||||
|
'not-allowed': ('cancel','405'),
|
||||||
|
'not-authorized': ('auth','401'),
|
||||||
|
'payment-required': ('auth','402'),
|
||||||
|
'recipient-unavailable': ('wait','404'),
|
||||||
|
'redirect': ('modify','302'),
|
||||||
|
'registration-required': ('auth','407'),
|
||||||
|
'remote-server-not-found': ('cancel','404'),
|
||||||
|
'remote-server-timeout': ('wait','504'),
|
||||||
|
'resource-constraint': ('wait','500'),
|
||||||
|
'service-unavailable': ('cancel','503'),
|
||||||
|
'subscription-required': ('auth','407'),
|
||||||
|
'undefined-condition': (None,'500'),
|
||||||
|
'unexpected-request': ('wait','400')}
|
||||||
|
|
||||||
|
def setup(self, xml):
|
||||||
|
"""Don't create XML for the plugin."""
|
||||||
|
self.xml = ET.Element('')
|
||||||
|
|
||||||
|
def set_condition(self, value):
|
||||||
|
"""
|
||||||
|
Set the error type and code based on the given error
|
||||||
|
condition value.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
value -- The new error condition.
|
||||||
|
"""
|
||||||
|
self.parent().set_condition(value)
|
||||||
|
|
||||||
|
error_data = self.error_map.get(value, None)
|
||||||
|
if error_data is not None:
|
||||||
|
if error_data[0] is not None:
|
||||||
|
self.parent()['type'] = error_data[0]
|
||||||
|
self.parent()['code'] = error_data[1]
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
"""
|
|
||||||
SleekXMPP: The Sleek XMPP Library
|
|
||||||
Copyright (C) 2010 Nathanael C. Fritz
|
|
||||||
This file is part of SleekXMPP.
|
|
||||||
|
|
||||||
See the file LICENSE for copying permission.
|
|
||||||
"""
|
|
||||||
from xml.etree import cElementTree as ET
|
|
||||||
from . import base
|
|
||||||
from .. xmlstream.handler.xmlwaiter import XMLWaiter
|
|
||||||
|
|
||||||
class xep_0092(base.base_plugin):
|
|
||||||
"""
|
|
||||||
XEP-0092 Software Version
|
|
||||||
"""
|
|
||||||
def plugin_init(self):
|
|
||||||
self.description = "Software Version"
|
|
||||||
self.xep = "0092"
|
|
||||||
self.name = self.config.get('name', 'SleekXMPP')
|
|
||||||
self.version = self.config.get('version', '0.1-dev')
|
|
||||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version, name='Sofware Version')
|
|
||||||
|
|
||||||
def post_init(self):
|
|
||||||
base.base_plugin.post_init(self)
|
|
||||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version')
|
|
||||||
|
|
||||||
def report_version(self, xml):
|
|
||||||
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
|
|
||||||
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
|
||||||
query = ET.Element('{jabber:iq:version}query')
|
|
||||||
name = ET.Element('name')
|
|
||||||
name.text = self.name
|
|
||||||
version = ET.Element('version')
|
|
||||||
version.text = self.version
|
|
||||||
query.append(name)
|
|
||||||
query.append(version)
|
|
||||||
iq.append(query)
|
|
||||||
self.xmpp.send(iq)
|
|
||||||
|
|
||||||
def getVersion(self, jid):
|
|
||||||
iq = self.xmpp.makeIqGet()
|
|
||||||
query = ET.Element('{jabber:iq:version}query')
|
|
||||||
iq.append(query)
|
|
||||||
iq.attrib['to'] = jid
|
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
|
||||||
id = iq.get('id')
|
|
||||||
result = iq.send()
|
|
||||||
if result and result is not None and result.get('type', 'error') != 'error':
|
|
||||||
qry = result.find('{jabber:iq:version}query')
|
|
||||||
version = {}
|
|
||||||
for child in qry.getchildren():
|
|
||||||
version[child.tag.split('}')[-1]] = child.text
|
|
||||||
return version
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
11
sleekxmpp/plugins/xep_0092/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0092/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0092 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0092.stanza import Version
|
||||||
|
from sleekxmpp.plugins.xep_0092.version import xep_0092
|
||||||
42
sleekxmpp/plugins/xep_0092/stanza.py
Normal file
42
sleekxmpp/plugins/xep_0092/stanza.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, ET
|
||||||
|
|
||||||
|
|
||||||
|
class Version(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XMPP allows for an agent to advertise the name and version of the
|
||||||
|
underlying software libraries, as well as the operating system
|
||||||
|
that the agent is running on.
|
||||||
|
|
||||||
|
Example version stanzas:
|
||||||
|
<iq type="get">
|
||||||
|
<query xmlns="jabber:iq:version" />
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
<iq type="result">
|
||||||
|
<query xmlns="jabber:iq:version">
|
||||||
|
<name>SleekXMPP</name>
|
||||||
|
<version>1.0</version>
|
||||||
|
<os>Linux</os>
|
||||||
|
</query>
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
name -- The human readable name of the software.
|
||||||
|
version -- The specific version of the software.
|
||||||
|
os -- The name of the operating system running the program.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'query'
|
||||||
|
namespace = 'jabber:iq:version'
|
||||||
|
plugin_attrib = 'software_version'
|
||||||
|
interfaces = set(('name', 'version', 'os'))
|
||||||
|
sub_interfaces = interfaces
|
||||||
88
sleekxmpp/plugins/xep_0092/version.py
Normal file
88
sleekxmpp/plugins/xep_0092/version.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0092 import Version
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0092(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0092: Software Version
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""
|
||||||
|
Start the XEP-0092 plugin.
|
||||||
|
"""
|
||||||
|
self.xep = "0092"
|
||||||
|
self.description = "Software Version"
|
||||||
|
self.stanza = sleekxmpp.plugins.xep_0092.stanza
|
||||||
|
|
||||||
|
self.name = self.config.get('name', 'SleekXMPP')
|
||||||
|
self.version = self.config.get('version', '0.1-dev')
|
||||||
|
self.os = self.config.get('os', '')
|
||||||
|
|
||||||
|
self.getVersion = self.get_version
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('Software Version',
|
||||||
|
StanzaPath('iq@type=get/software_version'),
|
||||||
|
self._handle_version))
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, Version)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""
|
||||||
|
Handle cross-plugin dependencies.
|
||||||
|
"""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version')
|
||||||
|
|
||||||
|
def _handle_version(self, iq):
|
||||||
|
"""
|
||||||
|
Respond to a software version query.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The Iq stanza containing the software version query.
|
||||||
|
"""
|
||||||
|
iq.reply()
|
||||||
|
iq['software_version']['name'] = self.name
|
||||||
|
iq['software_version']['version'] = self.version
|
||||||
|
iq['software_version']['os'] = self.os
|
||||||
|
iq.send()
|
||||||
|
|
||||||
|
def get_version(self, jid, ifrom=None):
|
||||||
|
"""
|
||||||
|
Retrieve the software version of a remote agent.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID of the entity to query.
|
||||||
|
"""
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['to'] = jid
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['query'] = Version.namespace
|
||||||
|
|
||||||
|
result = iq.send()
|
||||||
|
|
||||||
|
if result and result['type'] != 'error':
|
||||||
|
return result['software_version'].values
|
||||||
|
return False
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"""
|
|
||||||
SleekXMPP: The Sleek XMPP Library
|
|
||||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
|
||||||
This file is part of SleekXMPP.
|
|
||||||
|
|
||||||
See the file LICENSE for copying permission.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from . import base
|
|
||||||
from .. xmlstream.handler.callback import Callback
|
|
||||||
from .. xmlstream.matcher.xpath import MatchXPath
|
|
||||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
|
||||||
from .. stanza.iq import Iq
|
|
||||||
from . xep_0030 import DiscoInfo, DiscoItems
|
|
||||||
from . xep_0004 import Form
|
|
||||||
|
|
||||||
|
|
||||||
class xep_0128(base.base_plugin):
|
|
||||||
"""
|
|
||||||
XEP-0128 Service Discovery Extensions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def plugin_init(self):
|
|
||||||
self.xep = '0128'
|
|
||||||
self.description = 'Service Discovery Extensions'
|
|
||||||
|
|
||||||
registerStanzaPlugin(DiscoInfo, Form)
|
|
||||||
registerStanzaPlugin(DiscoItems, Form)
|
|
||||||
|
|
||||||
def extend_info(self, node, data=None):
|
|
||||||
if data is None:
|
|
||||||
data = {}
|
|
||||||
node = self.xmpp['xep_0030'].nodes.get(node, None)
|
|
||||||
if node is None:
|
|
||||||
self.xmpp['xep_0030'].add_node(node)
|
|
||||||
|
|
||||||
info = node.info
|
|
||||||
info['form']['type'] = 'result'
|
|
||||||
info['form'].setFields(data, default=None)
|
|
||||||
|
|
||||||
def extend_items(self, node, data=None):
|
|
||||||
if data is None:
|
|
||||||
data = {}
|
|
||||||
node = self.xmpp['xep_0030'].nodes.get(node, None)
|
|
||||||
if node is None:
|
|
||||||
self.xmpp['xep_0030'].add_node(node)
|
|
||||||
|
|
||||||
items = node.items
|
|
||||||
items['form']['type'] = 'result'
|
|
||||||
items['form'].setFields(data, default=None)
|
|
||||||
10
sleekxmpp/plugins/xep_0128/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0128/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0128.static import StaticExtendedDisco
|
||||||
|
from sleekxmpp.plugins.xep_0128.extended_disco import xep_0128
|
||||||
101
sleekxmpp/plugins/xep_0128/extended_disco.py
Normal file
101
sleekxmpp/plugins/xep_0128/extended_disco.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0004 import Form
|
||||||
|
from sleekxmpp.plugins.xep_0030 import DiscoInfo
|
||||||
|
from sleekxmpp.plugins.xep_0128 import StaticExtendedDisco
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0128(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0128: Service Discovery Extensions
|
||||||
|
|
||||||
|
Allow the use of data forms to add additional identity
|
||||||
|
information to disco#info results.
|
||||||
|
|
||||||
|
Also see <http://www.xmpp.org/extensions/xep-0128.html>.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
disco -- A reference to the XEP-0030 plugin.
|
||||||
|
static -- Object containing the default set of static
|
||||||
|
node handlers.
|
||||||
|
xmpp -- The main SleekXMPP object.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
set_extended_info -- Set extensions to a disco#info result.
|
||||||
|
add_extended_info -- Add an extension to a disco#info result.
|
||||||
|
del_extended_info -- Remove all extensions from a disco#info result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""Start the XEP-0128 plugin."""
|
||||||
|
self.xep = '0128'
|
||||||
|
self.description = 'Service Discovery Extensions'
|
||||||
|
|
||||||
|
self._disco_ops = ['set_extended_info',
|
||||||
|
'add_extended_info',
|
||||||
|
'del_extended_info']
|
||||||
|
|
||||||
|
register_stanza_plugin(DiscoInfo, Form, iterable=True)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Handle cross-plugin dependencies."""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.disco = self.xmpp['xep_0030']
|
||||||
|
self.static = StaticExtendedDisco(self.disco.static)
|
||||||
|
|
||||||
|
self.disco.set_extended_info = self.set_extended_info
|
||||||
|
self.disco.add_extended_info = self.add_extended_info
|
||||||
|
self.disco.del_extended_info = self.del_extended_info
|
||||||
|
|
||||||
|
for op in self._disco_ops:
|
||||||
|
self.disco._add_disco_op(op, getattr(self.static, op))
|
||||||
|
|
||||||
|
def set_extended_info(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Set additional, extended identity information to a node.
|
||||||
|
|
||||||
|
Replaces any existing extended information.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
data -- Either a form, or a list of forms to use
|
||||||
|
as extended information, replacing any
|
||||||
|
existing extensions.
|
||||||
|
"""
|
||||||
|
self.disco._run_node_handler('set_extended_info', jid, node, kwargs)
|
||||||
|
|
||||||
|
def add_extended_info(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Add additional, extended identity information to a node.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
data -- Either a form, or a list of forms to add
|
||||||
|
as extended information.
|
||||||
|
"""
|
||||||
|
self.disco._run_node_handler('add_extended_info', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_extended_info(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove all extended identity information to a node.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
"""
|
||||||
|
self.disco._run_node_handler('del_extended_info', jid, node, kwargs)
|
||||||
72
sleekxmpp/plugins/xep_0128/static.py
Normal file
72
sleekxmpp/plugins/xep_0128/static.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp.plugins.xep_0030 import StaticDisco
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class StaticExtendedDisco(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Extend the default StaticDisco implementation to provide
|
||||||
|
support for extended identity information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, static):
|
||||||
|
"""
|
||||||
|
Augment the default XEP-0030 static handler object.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
static -- The default static XEP-0030 handler object.
|
||||||
|
"""
|
||||||
|
self.static = static
|
||||||
|
|
||||||
|
def set_extended_info(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Replace the extended identity data for a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter may provide:
|
||||||
|
data -- Either a single data form, or a list of data forms.
|
||||||
|
"""
|
||||||
|
self.del_extended_info(jid, node, data)
|
||||||
|
self.add_extended_info(jid, node, data)
|
||||||
|
|
||||||
|
def add_extended_info(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Add additional extended identity data for a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter may provide:
|
||||||
|
data -- Either a single data form, or a list of data forms.
|
||||||
|
"""
|
||||||
|
self.static.add_node(jid, node)
|
||||||
|
|
||||||
|
forms = data.get('data', [])
|
||||||
|
if not isinstance(forms, list):
|
||||||
|
forms = [forms]
|
||||||
|
|
||||||
|
for form in forms:
|
||||||
|
self.static.nodes[(jid, node)]['info'].append(form)
|
||||||
|
|
||||||
|
def del_extended_info(self, jid, node, data):
|
||||||
|
"""
|
||||||
|
Replace the extended identity data for a JID/node combination.
|
||||||
|
|
||||||
|
The data parameter is not used.
|
||||||
|
"""
|
||||||
|
if (jid, node) not in self.static.nodes:
|
||||||
|
return
|
||||||
|
|
||||||
|
info = self.static.nodes[(jid, node)]['info']
|
||||||
|
|
||||||
|
for form in info['substanza']:
|
||||||
|
info.xml.remove(form.xml)
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
"""
|
|
||||||
SleekXMPP: The Sleek XMPP Library
|
|
||||||
Copyright (C) 2010 Nathanael C. Fritz
|
|
||||||
This file is part of SleekXMPP.
|
|
||||||
|
|
||||||
See the file LICENSE for copying permission.
|
|
||||||
"""
|
|
||||||
from xml.etree import cElementTree as ET
|
|
||||||
from . import base
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class xep_0199(base.base_plugin):
|
|
||||||
"""XEP-0199 XMPP Ping"""
|
|
||||||
|
|
||||||
def plugin_init(self):
|
|
||||||
self.description = "XMPP Ping"
|
|
||||||
self.xep = "0199"
|
|
||||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><ping xmlns='urn:xmpp:ping'/></iq>" % self.xmpp.default_ns, self.handler_ping, name='XMPP Ping')
|
|
||||||
if self.config.get('keepalive', True):
|
|
||||||
self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True)
|
|
||||||
|
|
||||||
def post_init(self):
|
|
||||||
base.base_plugin.post_init(self)
|
|
||||||
self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:ping')
|
|
||||||
|
|
||||||
def handler_pingserver(self, xml):
|
|
||||||
self.xmpp.schedule("xep-0119 ping", float(self.config.get('frequency', 300)), self.scheduled_ping, repeat=True)
|
|
||||||
|
|
||||||
def scheduled_ping(self):
|
|
||||||
log.debug("pinging...")
|
|
||||||
if self.sendPing(self.xmpp.server, self.config.get('timeout', 30)) is False:
|
|
||||||
log.debug("Did not recieve ping back in time. Requesting Reconnect.")
|
|
||||||
self.xmpp.reconnect()
|
|
||||||
|
|
||||||
def handler_ping(self, xml):
|
|
||||||
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
|
|
||||||
iq.attrib['to'] = xml.get('from', self.xmpp.boundjid.domain)
|
|
||||||
self.xmpp.send(iq)
|
|
||||||
|
|
||||||
def sendPing(self, jid, timeout = 30):
|
|
||||||
""" sendPing(jid, timeout)
|
|
||||||
Sends a ping to the specified jid, returning the time (in seconds)
|
|
||||||
to receive a reply, or None if no reply is received in timeout seconds.
|
|
||||||
"""
|
|
||||||
id = self.xmpp.getNewId()
|
|
||||||
iq = self.xmpp.makeIq(id)
|
|
||||||
iq.attrib['type'] = 'get'
|
|
||||||
iq.attrib['to'] = jid
|
|
||||||
ping = ET.Element('{urn:xmpp:ping}ping')
|
|
||||||
iq.append(ping)
|
|
||||||
startTime = time.clock()
|
|
||||||
#pingresult = self.xmpp.send(iq, self.xmpp.makeIq(id), timeout)
|
|
||||||
pingresult = iq.send()
|
|
||||||
endTime = time.clock()
|
|
||||||
if pingresult == False:
|
|
||||||
#self.xmpp.disconnect(reconnect=True)
|
|
||||||
return False
|
|
||||||
return endTime - startTime
|
|
||||||
10
sleekxmpp/plugins/xep_0199/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0199/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0199.stanza import Ping
|
||||||
|
from sleekxmpp.plugins.xep_0199.ping import xep_0199
|
||||||
163
sleekxmpp/plugins/xep_0199/ping.py
Normal file
163
sleekxmpp/plugins/xep_0199/ping.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0199 import stanza, Ping
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0199(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0199: XMPP Ping
|
||||||
|
|
||||||
|
Given that XMPP is based on TCP connections, it is possible for the
|
||||||
|
underlying connection to be terminated without the application's
|
||||||
|
awareness. Ping stanzas provide an alternative to whitespace based
|
||||||
|
keepalive methods for detecting lost connections.
|
||||||
|
|
||||||
|
Also see <http://www.xmpp.org/extensions/xep-0199.html>.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
keepalive -- If True, periodically send ping requests
|
||||||
|
to the server. If a ping is not answered,
|
||||||
|
the connection will be reset.
|
||||||
|
frequency -- Time in seconds between keepalive pings.
|
||||||
|
Defaults to 300 seconds.
|
||||||
|
timeout -- Time in seconds to wait for a ping response.
|
||||||
|
Defaults to 30 seconds.
|
||||||
|
Methods:
|
||||||
|
send_ping -- Send a ping to a given JID, returning the
|
||||||
|
round trip time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""
|
||||||
|
Start the XEP-0199 plugin.
|
||||||
|
"""
|
||||||
|
self.description = 'XMPP Ping'
|
||||||
|
self.xep = '0199'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
self.keepalive = self.config.get('keepalive', False)
|
||||||
|
self.frequency = float(self.config.get('frequency', 300))
|
||||||
|
self.timeout = self.config.get('timeout', 30)
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, Ping)
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('Ping',
|
||||||
|
StanzaPath('iq@type=get/ping'),
|
||||||
|
self._handle_ping))
|
||||||
|
|
||||||
|
if self.keepalive:
|
||||||
|
self.xmpp.add_event_handler('session_start',
|
||||||
|
self._handle_keepalive,
|
||||||
|
threaded=True)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Handle cross-plugin dependencies."""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp['xep_0030'].add_feature(Ping.namespace)
|
||||||
|
|
||||||
|
def _handle_keepalive(self, event):
|
||||||
|
"""
|
||||||
|
Begin periodic pinging of the server. If a ping is not
|
||||||
|
answered, the connection will be restarted.
|
||||||
|
|
||||||
|
The pinging interval can be adjused using self.frequency
|
||||||
|
before beginning processing.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
event -- The session_start event.
|
||||||
|
"""
|
||||||
|
def scheduled_ping():
|
||||||
|
"""Send ping request to the server."""
|
||||||
|
log.debug("Pinging...")
|
||||||
|
resp = self.send_ping(self.xmpp.boundjid.host, self.timeout)
|
||||||
|
if resp is None or resp is False:
|
||||||
|
log.debug("Did not recieve ping back in time." + \
|
||||||
|
"Requesting Reconnect.")
|
||||||
|
self.xmpp.reconnect()
|
||||||
|
|
||||||
|
self.xmpp.schedule('Ping Keep Alive',
|
||||||
|
self.frequency,
|
||||||
|
scheduled_ping,
|
||||||
|
repeat=True)
|
||||||
|
|
||||||
|
def _handle_ping(self, iq):
|
||||||
|
"""
|
||||||
|
Automatically reply to ping requests.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The ping request.
|
||||||
|
"""
|
||||||
|
log.debug("Pinged by %s" % iq['from'])
|
||||||
|
iq.reply().enable('ping').send()
|
||||||
|
|
||||||
|
def send_ping(self, jid, timeout=None, errorfalse=False,
|
||||||
|
ifrom=None, block=True, callback=None):
|
||||||
|
"""
|
||||||
|
Send a ping request and calculate the response time.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID that will receive the ping.
|
||||||
|
timeout -- Time in seconds to wait for a response.
|
||||||
|
Defaults to self.timeout.
|
||||||
|
errorfalse -- Indicates if False should be returned
|
||||||
|
if an error stanza is received. Defaults
|
||||||
|
to False.
|
||||||
|
ifrom -- Specifiy the sender JID.
|
||||||
|
block -- Indicate if execution should block until
|
||||||
|
a pong response is received. Defaults
|
||||||
|
to True.
|
||||||
|
callback -- Optional handler to execute when a pong
|
||||||
|
is received. Useful in conjunction with
|
||||||
|
the option block=False.
|
||||||
|
"""
|
||||||
|
log.debug("Pinging %s" % jid)
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.timeout
|
||||||
|
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['to'] = jid
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
iq.enable('ping')
|
||||||
|
|
||||||
|
start_time = time.clock()
|
||||||
|
resp = iq.send(block=block,
|
||||||
|
timeout=timeout,
|
||||||
|
callback=callback)
|
||||||
|
end_time = time.clock()
|
||||||
|
|
||||||
|
delay = end_time - start_time
|
||||||
|
|
||||||
|
if not block:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not resp or resp['type'] == 'error':
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.debug("Pong: %s %f" % (jid, delay))
|
||||||
|
return delay
|
||||||
|
|
||||||
|
|
||||||
|
# Backwards compatibility for names
|
||||||
|
xep_0199.sendPing = xep_0199.send_ping
|
||||||
36
sleekxmpp/plugins/xep_0199/stanza.py
Normal file
36
sleekxmpp/plugins/xep_0199/stanza.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp.xmlstream import ElementBase
|
||||||
|
|
||||||
|
|
||||||
|
class Ping(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Given that XMPP is based on TCP connections, it is possible for the
|
||||||
|
underlying connection to be terminated without the application's
|
||||||
|
awareness. Ping stanzas provide an alternative to whitespace based
|
||||||
|
keepalive methods for detecting lost connections.
|
||||||
|
|
||||||
|
Example ping stanza:
|
||||||
|
<iq type="get">
|
||||||
|
<ping xmlns="urn:xmpp:ping" />
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
None
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'ping'
|
||||||
|
namespace = 'urn:xmpp:ping'
|
||||||
|
plugin_attrib = 'ping'
|
||||||
|
interfaces = set()
|
||||||
@@ -27,10 +27,12 @@ class EntityTime(ElementBase):
|
|||||||
interfaces = set(('tzo', 'utc'))
|
interfaces = set(('tzo', 'utc'))
|
||||||
sub_interfaces = set(('tzo', 'utc'))
|
sub_interfaces = set(('tzo', 'utc'))
|
||||||
|
|
||||||
#def get_utc(self): # TODO: return a datetime.tzinfo object?
|
#def get_tzo(self):
|
||||||
|
# TODO: Right now it returns a string but maybe it should
|
||||||
|
# return a datetime.tzinfo object or maybe a datetime.timedelta?
|
||||||
#pass
|
#pass
|
||||||
|
|
||||||
def set_tzo(self, tzo): # TODO: support datetime.tzinfo objects?
|
def set_tzo(self, tzo):
|
||||||
if isinstance(tzo, tzinfo):
|
if isinstance(tzo, tzinfo):
|
||||||
td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here'
|
td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here'
|
||||||
seconds = td.seconds + td.days * 24 * 3600
|
seconds = td.seconds + td.days * 24 * 3600
|
||||||
@@ -45,7 +47,7 @@ class EntityTime(ElementBase):
|
|||||||
# Returns a datetime object instead the string. Is this a good idea?
|
# Returns a datetime object instead the string. Is this a good idea?
|
||||||
value = self._get_sub_text('utc')
|
value = self._get_sub_text('utc')
|
||||||
if '.' in value:
|
if '.' in value:
|
||||||
return datetime.strptime(value, '%Y-%m-%d.%fT%H:%M:%SZ')
|
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
|
||||||
else:
|
else:
|
||||||
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
|
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
|
||||||
|
|||||||
10
sleekxmpp/plugins/xep_0249/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0249/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Dalek
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.plugins.xep_0249.stanza import Invite
|
||||||
|
from sleekxmpp.plugins.xep_0249.invite import xep_0249
|
||||||
79
sleekxmpp/plugins/xep_0249/invite.py
Normal file
79
sleekxmpp/plugins/xep_0249/invite.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Dalek
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import sleekxmpp
|
||||||
|
from sleekxmpp import Message
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.plugins.xep_0249 import Invite
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0249(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0249: Direct MUC Invitations
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = "0249"
|
||||||
|
self.description = "Direct MUC Invitations"
|
||||||
|
self.stanza = sleekxmpp.plugins.xep_0249.stanza
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback('Direct MUC Invitations',
|
||||||
|
StanzaPath('message/groupchat_invite'),
|
||||||
|
self._handle_invite))
|
||||||
|
|
||||||
|
register_stanza_plugin(Message, Invite)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp['xep_0030'].add_feature(Invite.namespace)
|
||||||
|
|
||||||
|
def _handle_invite(self, msg):
|
||||||
|
"""
|
||||||
|
Raise an event for all invitations received.
|
||||||
|
"""
|
||||||
|
log.debug("Received direct muc invitation from %s to room %s",
|
||||||
|
msg['from'], msg['groupchat_invite']['jid'])
|
||||||
|
|
||||||
|
self.xmpp.event('groupchat_direct_invite', msg)
|
||||||
|
|
||||||
|
def send_invitation(self, jid, roomjid, password=None,
|
||||||
|
reason=None, ifrom=None):
|
||||||
|
"""
|
||||||
|
Send a direct MUC invitation to an XMPP entity.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID of the entity that will receive
|
||||||
|
the invitation
|
||||||
|
roomjid -- the address of the groupchat room to be joined
|
||||||
|
password -- a password needed for entry into a
|
||||||
|
password-protected room (OPTIONAL).
|
||||||
|
reason -- a human-readable purpose for the invitation
|
||||||
|
(OPTIONAL).
|
||||||
|
"""
|
||||||
|
|
||||||
|
msg = self.xmpp.Message()
|
||||||
|
msg['to'] = jid
|
||||||
|
if ifrom is not None:
|
||||||
|
msg['from'] = ifrom
|
||||||
|
msg['groupchat_invite']['jid'] = roomjid
|
||||||
|
if password is not None:
|
||||||
|
msg['groupchat_invite']['password'] = password
|
||||||
|
if reason is not None:
|
||||||
|
msg['groupchat_invite']['reason'] = reason
|
||||||
|
|
||||||
|
return msg.send()
|
||||||
39
sleekxmpp/plugins/xep_0249/stanza.py
Normal file
39
sleekxmpp/plugins/xep_0249/stanza.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2011 Nathanael C. Fritz, Dalek
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.xmlstream import ElementBase
|
||||||
|
|
||||||
|
|
||||||
|
class Invite(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XMPP allows for an agent in an MUC room to directly invite another
|
||||||
|
user to join the chat room (as opposed to a mediated invitation
|
||||||
|
done through the server).
|
||||||
|
|
||||||
|
Example invite stanza:
|
||||||
|
<message from='crone1@shakespeare.lit/desktop'
|
||||||
|
to='hecate@shakespeare.lit'>
|
||||||
|
<x xmlns='jabber:x:conference'
|
||||||
|
jid='darkcave@macbeth.shakespeare.lit'
|
||||||
|
password='cauldronburn'
|
||||||
|
reason='Hey Hecate, this is the place for all good witches!'/>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
jid -- The JID of the groupchat room
|
||||||
|
password -- The password used to gain entry in the room
|
||||||
|
(optional)
|
||||||
|
reason -- The reason for the invitation (optional)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "x"
|
||||||
|
namespace = "jabber:x:conference"
|
||||||
|
plugin_attrib = "groupchat_invite"
|
||||||
|
interfaces = ("jid", "password", "reason")
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from sleekxmpp.stanza.error import Error
|
from sleekxmpp.stanza.error import Error
|
||||||
|
from sleekxmpp.stanza.stream_error import StreamError
|
||||||
from sleekxmpp.stanza.iq import Iq
|
from sleekxmpp.stanza.iq import Iq
|
||||||
from sleekxmpp.stanza.message import Message
|
from sleekxmpp.stanza.message import Message
|
||||||
from sleekxmpp.stanza.presence import Presence
|
from sleekxmpp.stanza.presence import Presence
|
||||||
|
|||||||
@@ -77,15 +77,6 @@ class Error(ElementBase):
|
|||||||
Arguments:
|
Arguments:
|
||||||
xml -- Use an existing XML object for the stanza's values.
|
xml -- Use an existing XML object for the stanza's values.
|
||||||
"""
|
"""
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.getCondition = self.get_condition
|
|
||||||
self.setCondition = self.set_condition
|
|
||||||
self.delCondition = self.del_condition
|
|
||||||
self.getText = self.get_text
|
|
||||||
self.setText = self.set_text
|
|
||||||
self.delText = self.del_text
|
|
||||||
|
|
||||||
if ElementBase.setup(self, xml):
|
if ElementBase.setup(self, xml):
|
||||||
#If we had to generate XML then set default values.
|
#If we had to generate XML then set default values.
|
||||||
self['type'] = 'cancel'
|
self['type'] = 'cancel'
|
||||||
@@ -139,3 +130,13 @@ class Error(ElementBase):
|
|||||||
"""Remove the <text> element."""
|
"""Remove the <text> element."""
|
||||||
self._del_sub('{%s}text' % self.condition_ns)
|
self._del_sub('{%s}text' % self.condition_ns)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
Error.getCondition = Error.get_condition
|
||||||
|
Error.setCondition = Error.set_condition
|
||||||
|
Error.delCondition = Error.del_condition
|
||||||
|
Error.getText = Error.get_text
|
||||||
|
Error.setText = Error.set_text
|
||||||
|
Error.delText = Error.del_text
|
||||||
|
|||||||
@@ -46,23 +46,6 @@ class HTMLIM(ElementBase):
|
|||||||
interfaces = set(('body',))
|
interfaces = set(('body',))
|
||||||
plugin_attrib = name
|
plugin_attrib = name
|
||||||
|
|
||||||
def setup(self, xml=None):
|
|
||||||
"""
|
|
||||||
Populate the stanza object using an optional XML object.
|
|
||||||
|
|
||||||
Overrides StanzaBase.setup.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- Use an existing XML object for the stanza's values.
|
|
||||||
"""
|
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.setBody = self.set_body
|
|
||||||
self.getBody = self.get_body
|
|
||||||
self.delBody = self.del_body
|
|
||||||
|
|
||||||
return ElementBase.setup(self, xml)
|
|
||||||
|
|
||||||
def set_body(self, html):
|
def set_body(self, html):
|
||||||
"""
|
"""
|
||||||
Set the contents of the HTML body.
|
Set the contents of the HTML body.
|
||||||
@@ -95,3 +78,9 @@ class HTMLIM(ElementBase):
|
|||||||
|
|
||||||
|
|
||||||
register_stanza_plugin(Message, HTMLIM)
|
register_stanza_plugin(Message, HTMLIM)
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
HTMLIM.setBody = HTMLIM.set_body
|
||||||
|
HTMLIM.getBody = HTMLIM.get_body
|
||||||
|
HTMLIM.delBody = HTMLIM.del_body
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
from sleekxmpp.stanza import Error
|
from sleekxmpp.stanza import Error
|
||||||
from sleekxmpp.stanza.rootstanza import RootStanza
|
from sleekxmpp.stanza.rootstanza import RootStanza
|
||||||
from sleekxmpp.xmlstream import RESPONSE_TIMEOUT, StanzaBase, ET
|
from sleekxmpp.xmlstream import StanzaBase, ET
|
||||||
from sleekxmpp.xmlstream.handler import Waiter
|
from sleekxmpp.xmlstream.handler import Waiter, Callback
|
||||||
from sleekxmpp.xmlstream.matcher import MatcherId
|
from sleekxmpp.xmlstream.matcher import MatcherId
|
||||||
|
|
||||||
|
|
||||||
@@ -75,16 +75,9 @@ class Iq(RootStanza):
|
|||||||
Overrides StanzaBase.__init__.
|
Overrides StanzaBase.__init__.
|
||||||
"""
|
"""
|
||||||
StanzaBase.__init__(self, *args, **kwargs)
|
StanzaBase.__init__(self, *args, **kwargs)
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.setPayload = self.set_payload
|
|
||||||
self.getQuery = self.get_query
|
|
||||||
self.setQuery = self.set_query
|
|
||||||
self.delQuery = self.del_query
|
|
||||||
|
|
||||||
if self['id'] == '':
|
if self['id'] == '':
|
||||||
if self.stream is not None:
|
if self.stream is not None:
|
||||||
self['id'] = self.stream.getNewId()
|
self['id'] = self.stream.new_id()
|
||||||
else:
|
else:
|
||||||
self['id'] = '0'
|
self['id'] = '0'
|
||||||
|
|
||||||
@@ -144,7 +137,7 @@ class Iq(RootStanza):
|
|||||||
self.xml.remove(child)
|
self.xml.remove(child)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def reply(self):
|
def reply(self, clear=True):
|
||||||
"""
|
"""
|
||||||
Send a reply <iq> stanza.
|
Send a reply <iq> stanza.
|
||||||
|
|
||||||
@@ -152,32 +145,91 @@ class Iq(RootStanza):
|
|||||||
|
|
||||||
Sets the 'type' to 'result' in addition to the default
|
Sets the 'type' to 'result' in addition to the default
|
||||||
StanzaBase.reply behavior.
|
StanzaBase.reply behavior.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
clear -- Indicates if existing content should be
|
||||||
|
removed before replying. Defaults to True.
|
||||||
"""
|
"""
|
||||||
self['type'] = 'result'
|
self['type'] = 'result'
|
||||||
StanzaBase.reply(self)
|
StanzaBase.reply(self, clear)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def send(self, block=True, timeout=RESPONSE_TIMEOUT):
|
def send(self, block=True, timeout=None, callback=None, now=False):
|
||||||
"""
|
"""
|
||||||
Send an <iq> stanza over the XML stream.
|
Send an <iq> stanza over the XML stream.
|
||||||
|
|
||||||
The send call can optionally block until a response is received or
|
The send call can optionally block until a response is received or
|
||||||
a timeout occurs. Be aware that using blocking in non-threaded event
|
a timeout occurs. Be aware that using blocking in non-threaded event
|
||||||
handlers can drastically impact performance.
|
handlers can drastically impact performance. Otherwise, a callback
|
||||||
|
handler can be provided that will be executed when the Iq stanza's
|
||||||
|
result reply is received. Be aware though that that the callback
|
||||||
|
handler will not be executed in its own thread.
|
||||||
|
|
||||||
|
Using both block and callback is not recommended, and only the
|
||||||
|
callback argument will be used in that case.
|
||||||
|
|
||||||
Overrides StanzaBase.send
|
Overrides StanzaBase.send
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
block -- Specify if the send call will block until a response
|
block -- Specify if the send call will block until a response
|
||||||
is received, or a timeout occurs. Defaults to True.
|
is received, or a timeout occurs. Defaults to True.
|
||||||
timeout -- The length of time (in seconds) to wait for a response
|
timeout -- The length of time (in seconds) to wait for a response
|
||||||
before exiting the send call if blocking is used.
|
before exiting the send call if blocking is used.
|
||||||
Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
|
Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
|
||||||
|
callback -- Optional reference to a stream handler function. Will
|
||||||
|
be executed when a reply stanza is received.
|
||||||
|
now -- Indicates if the send queue should be skipped and send
|
||||||
|
the stanza immediately. Used during stream
|
||||||
|
initialization. Defaults to False.
|
||||||
"""
|
"""
|
||||||
if block and self['type'] in ('get', 'set'):
|
if timeout is None:
|
||||||
|
timeout = self.stream.response_timeout
|
||||||
|
if callback is not None and self['type'] in ('get', 'set'):
|
||||||
|
handler_name = 'IqCallback_%s' % self['id']
|
||||||
|
handler = Callback(handler_name,
|
||||||
|
MatcherId(self['id']),
|
||||||
|
callback,
|
||||||
|
once=True)
|
||||||
|
self.stream.register_handler(handler)
|
||||||
|
StanzaBase.send(self, now=now)
|
||||||
|
return handler_name
|
||||||
|
elif block and self['type'] in ('get', 'set'):
|
||||||
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
|
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
|
||||||
self.stream.registerHandler(waitfor)
|
self.stream.register_handler(waitfor)
|
||||||
StanzaBase.send(self)
|
StanzaBase.send(self, now=now)
|
||||||
return waitfor.wait(timeout)
|
return waitfor.wait(timeout)
|
||||||
else:
|
else:
|
||||||
return StanzaBase.send(self)
|
return StanzaBase.send(self, now=now)
|
||||||
|
|
||||||
|
def _set_stanza_values(self, values):
|
||||||
|
"""
|
||||||
|
Set multiple stanza interface values using a dictionary.
|
||||||
|
|
||||||
|
Stanza plugin values may be set usind nested dictionaries.
|
||||||
|
|
||||||
|
If the interface 'query' is given, then it will be set
|
||||||
|
last to avoid duplication of the <query /> element.
|
||||||
|
|
||||||
|
Overrides ElementBase._set_stanza_values.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
values -- A dictionary mapping stanza interface with values.
|
||||||
|
Plugin interfaces may accept a nested dictionary that
|
||||||
|
will be used recursively.
|
||||||
|
"""
|
||||||
|
query = values.get('query', '')
|
||||||
|
if query:
|
||||||
|
del values['query']
|
||||||
|
StanzaBase._set_stanza_values(self, values)
|
||||||
|
self['query'] = query
|
||||||
|
else:
|
||||||
|
StanzaBase._set_stanza_values(self, values)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
Iq.setPayload = Iq.set_payload
|
||||||
|
Iq.getQuery = Iq.get_query
|
||||||
|
Iq.setQuery = Iq.set_query
|
||||||
|
Iq.delQuery = Iq.del_query
|
||||||
|
|||||||
@@ -63,27 +63,6 @@ class Message(RootStanza):
|
|||||||
plugin_attrib = name
|
plugin_attrib = name
|
||||||
types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
|
types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
|
||||||
|
|
||||||
def setup(self, xml=None):
|
|
||||||
"""
|
|
||||||
Populate the stanza object using an optional XML object.
|
|
||||||
|
|
||||||
Overrides StanzaBase.setup.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- Use an existing XML object for the stanza's values.
|
|
||||||
"""
|
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.getType = self.get_type
|
|
||||||
self.getMucroom = self.get_mucroom
|
|
||||||
self.setMucroom = self.set_mucroom
|
|
||||||
self.delMucroom = self.del_mucroom
|
|
||||||
self.getMucnick = self.get_mucnick
|
|
||||||
self.setMucnick = self.set_mucnick
|
|
||||||
self.delMucnick = self.del_mucnick
|
|
||||||
|
|
||||||
return StanzaBase.setup(self, xml)
|
|
||||||
|
|
||||||
def get_type(self):
|
def get_type(self):
|
||||||
"""
|
"""
|
||||||
Return the message type.
|
Return the message type.
|
||||||
@@ -104,7 +83,7 @@ class Message(RootStanza):
|
|||||||
self['type'] = 'normal'
|
self['type'] = 'normal'
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def reply(self, body=None):
|
def reply(self, body=None, clear=True):
|
||||||
"""
|
"""
|
||||||
Create a message reply.
|
Create a message reply.
|
||||||
|
|
||||||
@@ -114,9 +93,11 @@ class Message(RootStanza):
|
|||||||
adds a message body if one is given.
|
adds a message body if one is given.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
body -- Optional text content for the message.
|
body -- Optional text content for the message.
|
||||||
|
clear -- Indicates if existing content should be removed
|
||||||
|
before replying. Defaults to True.
|
||||||
"""
|
"""
|
||||||
StanzaBase.reply(self)
|
StanzaBase.reply(self, clear)
|
||||||
if self['type'] == 'groupchat':
|
if self['type'] == 'groupchat':
|
||||||
self['to'] = self['to'].bare
|
self['to'] = self['to'].bare
|
||||||
|
|
||||||
@@ -163,3 +144,14 @@ class Message(RootStanza):
|
|||||||
def del_mucnick(self):
|
def del_mucnick(self):
|
||||||
"""Dummy method to prevent deletion."""
|
"""Dummy method to prevent deletion."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
Message.getType = Message.get_type
|
||||||
|
Message.getMucroom = Message.get_mucroom
|
||||||
|
Message.setMucroom = Message.set_mucroom
|
||||||
|
Message.delMucroom = Message.del_mucroom
|
||||||
|
Message.getMucnick = Message.get_mucnick
|
||||||
|
Message.setMucnick = Message.set_mucnick
|
||||||
|
Message.delMucnick = Message.del_mucnick
|
||||||
|
|||||||
@@ -44,28 +44,11 @@ class Nick(ElementBase):
|
|||||||
del_nick -- Remove the <nick> element.
|
del_nick -- Remove the <nick> element.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
namespace = 'http://jabber.org/nick/nick'
|
namespace = 'http://jabber.org/protocol/nick'
|
||||||
name = 'nick'
|
name = 'nick'
|
||||||
plugin_attrib = name
|
plugin_attrib = name
|
||||||
interfaces = set(('nick',))
|
interfaces = set(('nick',))
|
||||||
|
|
||||||
def setup(self, xml=None):
|
|
||||||
"""
|
|
||||||
Populate the stanza object using an optional XML object.
|
|
||||||
|
|
||||||
Overrides StanzaBase.setup.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- Use an existing XML object for the stanza's values.
|
|
||||||
"""
|
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.setNick = self.set_nick
|
|
||||||
self.getNick = self.get_nick
|
|
||||||
self.delNick = self.del_nick
|
|
||||||
|
|
||||||
return ElementBase.setup(self, xml)
|
|
||||||
|
|
||||||
def set_nick(self, nick):
|
def set_nick(self, nick):
|
||||||
"""
|
"""
|
||||||
Add a <nick> element with the given nickname.
|
Add a <nick> element with the given nickname.
|
||||||
@@ -87,3 +70,9 @@ class Nick(ElementBase):
|
|||||||
|
|
||||||
register_stanza_plugin(Message, Nick)
|
register_stanza_plugin(Message, Nick)
|
||||||
register_stanza_plugin(Presence, Nick)
|
register_stanza_plugin(Presence, Nick)
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
Nick.setNick = Nick.set_nick
|
||||||
|
Nick.getNick = Nick.get_nick
|
||||||
|
Nick.delNick = Nick.del_nick
|
||||||
|
|||||||
@@ -72,26 +72,6 @@ class Presence(RootStanza):
|
|||||||
'subscribed', 'unsubscribe', 'unsubscribed'))
|
'subscribed', 'unsubscribe', 'unsubscribed'))
|
||||||
showtypes = set(('dnd', 'chat', 'xa', 'away'))
|
showtypes = set(('dnd', 'chat', 'xa', 'away'))
|
||||||
|
|
||||||
def setup(self, xml=None):
|
|
||||||
"""
|
|
||||||
Populate the stanza object using an optional XML object.
|
|
||||||
|
|
||||||
Overrides ElementBase.setup.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- Use an existing XML object for the stanza's values.
|
|
||||||
"""
|
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.setShow = self.set_show
|
|
||||||
self.getType = self.get_type
|
|
||||||
self.setType = self.set_type
|
|
||||||
self.delType = self.get_type
|
|
||||||
self.getPriority = self.get_priority
|
|
||||||
self.setPriority = self.set_priority
|
|
||||||
|
|
||||||
return StanzaBase.setup(self, xml)
|
|
||||||
|
|
||||||
def exception(self, e):
|
def exception(self, e):
|
||||||
"""
|
"""
|
||||||
Override exception passback for presence.
|
Override exception passback for presence.
|
||||||
@@ -173,14 +153,28 @@ class Presence(RootStanza):
|
|||||||
# The priority is not a number: we consider it 0 as a default
|
# The priority is not a number: we consider it 0 as a default
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def reply(self):
|
def reply(self, clear=True):
|
||||||
"""
|
"""
|
||||||
Set the appropriate presence reply type.
|
Set the appropriate presence reply type.
|
||||||
|
|
||||||
Overrides StanzaBase.reply.
|
Overrides StanzaBase.reply.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
clear -- Indicates if the stanza contents should be removed
|
||||||
|
before replying. Defaults to True.
|
||||||
"""
|
"""
|
||||||
if self['type'] == 'unsubscribe':
|
if self['type'] == 'unsubscribe':
|
||||||
self['type'] = 'unsubscribed'
|
self['type'] = 'unsubscribed'
|
||||||
elif self['type'] == 'subscribe':
|
elif self['type'] == 'subscribe':
|
||||||
self['type'] = 'subscribed'
|
self['type'] = 'subscribed'
|
||||||
return StanzaBase.reply(self)
|
return StanzaBase.reply(self, clear)
|
||||||
|
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
Presence.setShow = Presence.set_show
|
||||||
|
Presence.getType = Presence.get_type
|
||||||
|
Presence.setType = Presence.set_type
|
||||||
|
Presence.delType = Presence.get_type
|
||||||
|
Presence.getPriority = Presence.get_priority
|
||||||
|
Presence.setPriority = Presence.set_priority
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ class RootStanza(StanzaBase):
|
|||||||
Arguments:
|
Arguments:
|
||||||
e -- Exception object
|
e -- Exception object
|
||||||
"""
|
"""
|
||||||
self.reply()
|
|
||||||
if isinstance(e, XMPPError):
|
if isinstance(e, XMPPError):
|
||||||
|
self.reply(clear=e.clear)
|
||||||
# We raised this deliberately
|
# We raised this deliberately
|
||||||
self['error']['condition'] = e.condition
|
self['error']['condition'] = e.condition
|
||||||
self['error']['text'] = e.text
|
self['error']['text'] = e.text
|
||||||
@@ -54,16 +54,18 @@ class RootStanza(StanzaBase):
|
|||||||
e.extension_args)
|
e.extension_args)
|
||||||
self['error'].append(extxml)
|
self['error'].append(extxml)
|
||||||
self['error']['type'] = e.etype
|
self['error']['type'] = e.etype
|
||||||
|
self.send()
|
||||||
else:
|
else:
|
||||||
# We probably didn't raise this on purpose, so send a traceback
|
self.reply()
|
||||||
|
# We probably didn't raise this on purpose, so send an error stanza
|
||||||
self['error']['condition'] = 'undefined-condition'
|
self['error']['condition'] = 'undefined-condition'
|
||||||
if sys.version_info < (3, 0):
|
self['error']['text'] = "SleekXMPP got into trouble."
|
||||||
self['error']['text'] = "SleekXMPP got into trouble."
|
self.send()
|
||||||
else:
|
# log the error
|
||||||
self['error']['text'] = traceback.format_tb(e.__traceback__)
|
log.exception('Error handling {%s}%s stanza' %
|
||||||
log.exception('Error handling {%s}%s stanza' %
|
(self.namespace, self.name))
|
||||||
(self.namespace, self.name))
|
# Finally raise the exception, so it can be handled (or not)
|
||||||
self.send()
|
# at a higher level by using sys.excepthook.
|
||||||
|
raise e
|
||||||
|
|
||||||
register_stanza_plugin(RootStanza, Error)
|
register_stanza_plugin(RootStanza, Error)
|
||||||
|
|||||||
@@ -38,23 +38,6 @@ class Roster(ElementBase):
|
|||||||
plugin_attrib = 'roster'
|
plugin_attrib = 'roster'
|
||||||
interfaces = set(('items',))
|
interfaces = set(('items',))
|
||||||
|
|
||||||
def setup(self, xml=None):
|
|
||||||
"""
|
|
||||||
Populate the stanza object using an optional XML object.
|
|
||||||
|
|
||||||
Overrides StanzaBase.setup.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
xml -- Use an existing XML object for the stanza's values.
|
|
||||||
"""
|
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.setItems = self.set_items
|
|
||||||
self.getItems = self.get_items
|
|
||||||
self.delItems = self.del_items
|
|
||||||
|
|
||||||
return ElementBase.setup(self, xml)
|
|
||||||
|
|
||||||
def set_items(self, items):
|
def set_items(self, items):
|
||||||
"""
|
"""
|
||||||
Set the roster entries in the <roster> stanza.
|
Set the roster entries in the <roster> stanza.
|
||||||
@@ -123,3 +106,9 @@ class Roster(ElementBase):
|
|||||||
|
|
||||||
|
|
||||||
register_stanza_plugin(Iq, Roster)
|
register_stanza_plugin(Iq, Roster)
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
Roster.setItems = Roster.set_items
|
||||||
|
Roster.getItems = Roster.get_items
|
||||||
|
Roster.delItems = Roster.del_items
|
||||||
|
|||||||
69
sleekxmpp/stanza/stream_error.py
Normal file
69
sleekxmpp/stanza/stream_error.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2010 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sleekxmpp.stanza.error import Error
|
||||||
|
from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class StreamError(Error, StanzaBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XMPP stanzas of type 'error' should include an <error> stanza that
|
||||||
|
describes the nature of the error and how it should be handled.
|
||||||
|
|
||||||
|
Use the 'XEP-0086: Error Condition Mappings' plugin to include error
|
||||||
|
codes used in older XMPP versions.
|
||||||
|
|
||||||
|
The stream:error stanza is used to provide more information for
|
||||||
|
error that occur with the underlying XML stream itself, and not
|
||||||
|
a particular stanza.
|
||||||
|
|
||||||
|
Note: The StreamError stanza is mostly the same as the normal
|
||||||
|
Error stanza, but with different namespaces and
|
||||||
|
condition names.
|
||||||
|
|
||||||
|
Example error stanza:
|
||||||
|
<stream:error>
|
||||||
|
<not-well-formed xmlns="urn:ietf:params:xml:ns:xmpp-streams" />
|
||||||
|
<text xmlns="urn:ietf:params:xml:ns:xmpp-streams">
|
||||||
|
XML was not well-formed.
|
||||||
|
</text>
|
||||||
|
</stream:error>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
condition -- The name of the condition element.
|
||||||
|
text -- Human readable description of the error.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
conditions -- The set of allowable error condition elements.
|
||||||
|
condition_ns -- The namespace for the condition element.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
setup -- Overrides ElementBase.setup.
|
||||||
|
get_condition -- Retrieve the name of the condition element.
|
||||||
|
set_condition -- Add a condition element.
|
||||||
|
del_condition -- Remove the condition element.
|
||||||
|
get_text -- Retrieve the contents of the <text> element.
|
||||||
|
set_text -- Set the contents of the <text> element.
|
||||||
|
del_text -- Remove the <text> element.
|
||||||
|
"""
|
||||||
|
|
||||||
|
namespace = 'http://etherx.jabber.org/streams'
|
||||||
|
interfaces = set(('condition', 'text'))
|
||||||
|
conditions = set((
|
||||||
|
'bad-format', 'bad-namespace-prefix', 'conflict',
|
||||||
|
'connection-timeout', 'host-gone', 'host-unknown',
|
||||||
|
'improper-addressing', 'internal-server-error', 'invalid-from',
|
||||||
|
'invalid-namespace', 'invalid-xml', 'not-authorized',
|
||||||
|
'not-well-formed', 'policy-violation', 'remote-connection-failed',
|
||||||
|
'reset', 'resource-constraint', 'restricted-xml', 'see-other-host',
|
||||||
|
'system-shutdown', 'undefined-condition', 'unsupported-encoding',
|
||||||
|
'unsupported-feature', 'unsupported-stanza-type',
|
||||||
|
'unsupported-version'))
|
||||||
|
condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams'
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
|
import threading
|
||||||
try:
|
try:
|
||||||
import queue
|
import queue
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -40,6 +41,8 @@ class TestLiveSocket(object):
|
|||||||
self.recv_buffer = []
|
self.recv_buffer = []
|
||||||
self.recv_queue = queue.Queue()
|
self.recv_queue = queue.Queue()
|
||||||
self.send_queue = queue.Queue()
|
self.send_queue = queue.Queue()
|
||||||
|
self.send_queue_lock = threading.Lock()
|
||||||
|
self.recv_queue_lock = threading.Lock()
|
||||||
self.is_live = True
|
self.is_live = True
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
@@ -55,6 +58,18 @@ class TestLiveSocket(object):
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Testing Interface
|
# Testing Interface
|
||||||
|
|
||||||
|
def disconnect_errror(self):
|
||||||
|
"""
|
||||||
|
Used to simulate a socket disconnection error.
|
||||||
|
|
||||||
|
Not used by live sockets.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.socket.shutdown()
|
||||||
|
self.socket.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def next_sent(self, timeout=None):
|
def next_sent(self, timeout=None):
|
||||||
"""
|
"""
|
||||||
Get the next stanza that has been sent.
|
Get the next stanza that has been sent.
|
||||||
@@ -108,7 +123,8 @@ class TestLiveSocket(object):
|
|||||||
Placeholders. Same as for socket.recv.
|
Placeholders. Same as for socket.recv.
|
||||||
"""
|
"""
|
||||||
data = self.socket.recv(*args, **kwargs)
|
data = self.socket.recv(*args, **kwargs)
|
||||||
self.recv_queue.put(data)
|
with self.recv_queue_lock:
|
||||||
|
self.recv_queue.put(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def send(self, data):
|
def send(self, data):
|
||||||
@@ -120,7 +136,8 @@ class TestLiveSocket(object):
|
|||||||
Arguments:
|
Arguments:
|
||||||
data -- String value to write.
|
data -- String value to write.
|
||||||
"""
|
"""
|
||||||
self.send_queue.put(data)
|
with self.send_queue_lock:
|
||||||
|
self.send_queue.put(data)
|
||||||
self.socket.send(data)
|
self.socket.send(data)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -143,3 +160,15 @@ class TestLiveSocket(object):
|
|||||||
Placeholders, same as socket.recv()
|
Placeholders, same as socket.recv()
|
||||||
"""
|
"""
|
||||||
return self.recv(*args, **kwargs)
|
return self.recv(*args, **kwargs)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Empty the send queue, typically done once the session has started to
|
||||||
|
remove the feature negotiation and log in stanzas.
|
||||||
|
"""
|
||||||
|
with self.send_queue_lock:
|
||||||
|
for i in range(0, self.send_queue.qsize()):
|
||||||
|
self.send_queue.get(block=False)
|
||||||
|
with self.recv_queue_lock:
|
||||||
|
for i in range(0, self.recv_queue.qsize()):
|
||||||
|
self.recv_queue.get(block=False)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class TestSocket(object):
|
|||||||
self.recv_queue = queue.Queue()
|
self.recv_queue = queue.Queue()
|
||||||
self.send_queue = queue.Queue()
|
self.send_queue = queue.Queue()
|
||||||
self.is_live = False
|
self.is_live = False
|
||||||
|
self.disconnected = False
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
"""
|
"""
|
||||||
@@ -89,6 +90,13 @@ class TestSocket(object):
|
|||||||
"""
|
"""
|
||||||
self.recv_queue.put(data)
|
self.recv_queue.put(data)
|
||||||
|
|
||||||
|
def disconnect_error(self):
|
||||||
|
"""
|
||||||
|
Simulate a disconnect error by raising a socket.error exception
|
||||||
|
for any current or further socket operations.
|
||||||
|
"""
|
||||||
|
self.disconnected = True
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Socket Interface
|
# Socket Interface
|
||||||
|
|
||||||
@@ -99,6 +107,8 @@ class TestSocket(object):
|
|||||||
Arguments:
|
Arguments:
|
||||||
Placeholders. Same as for socket.Socket.recv.
|
Placeholders. Same as for socket.Socket.recv.
|
||||||
"""
|
"""
|
||||||
|
if self.disconnected:
|
||||||
|
raise socket.error
|
||||||
return self.read(block=True)
|
return self.read(block=True)
|
||||||
|
|
||||||
def send(self, data):
|
def send(self, data):
|
||||||
@@ -108,6 +118,8 @@ class TestSocket(object):
|
|||||||
Arguments:
|
Arguments:
|
||||||
data -- String value to write.
|
data -- String value to write.
|
||||||
"""
|
"""
|
||||||
|
if self.disconnected:
|
||||||
|
raise socket.error
|
||||||
self.send_queue.put(data)
|
self.send_queue.put(data)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -132,6 +144,8 @@ class TestSocket(object):
|
|||||||
timeout -- Time in seconds a block should last before
|
timeout -- Time in seconds a block should last before
|
||||||
returning None.
|
returning None.
|
||||||
"""
|
"""
|
||||||
|
if self.disconnected:
|
||||||
|
raise socket.error
|
||||||
if timeout is not None:
|
if timeout is not None:
|
||||||
block = True
|
block = True
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -7,13 +7,20 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
try:
|
||||||
|
import Queue as queue
|
||||||
|
except:
|
||||||
|
import queue
|
||||||
|
|
||||||
import sleekxmpp
|
import sleekxmpp
|
||||||
from sleekxmpp import ClientXMPP, ComponentXMPP
|
from sleekxmpp import ClientXMPP, ComponentXMPP
|
||||||
from sleekxmpp.stanza import Message, Iq, Presence
|
from sleekxmpp.stanza import Message, Iq, Presence
|
||||||
from sleekxmpp.test import TestSocket, TestLiveSocket
|
from sleekxmpp.test import TestSocket, TestLiveSocket
|
||||||
from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
|
from sleekxmpp.xmlstream import ET, register_stanza_plugin
|
||||||
|
from sleekxmpp.xmlstream import ElementBase, StanzaBase
|
||||||
from sleekxmpp.xmlstream.tostring import tostring
|
from sleekxmpp.xmlstream.tostring import tostring
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId
|
||||||
|
from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
|
||||||
|
|
||||||
|
|
||||||
class SleekTest(unittest.TestCase):
|
class SleekTest(unittest.TestCase):
|
||||||
@@ -46,6 +53,10 @@ class SleekTest(unittest.TestCase):
|
|||||||
compare -- Compare XML objects against each other.
|
compare -- Compare XML objects against each other.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||||
|
self.xmpp = None
|
||||||
|
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -67,6 +78,8 @@ class SleekTest(unittest.TestCase):
|
|||||||
xml = self.parse_xml(xml_string)
|
xml = self.parse_xml(xml_string)
|
||||||
xml = xml.getchildren()[0]
|
xml = xml.getchildren()[0]
|
||||||
return xml
|
return xml
|
||||||
|
else:
|
||||||
|
self.fail("XML data was mal-formed:\n%s" % xml_string)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Shortcut methods for creating stanza objects
|
# Shortcut methods for creating stanza objects
|
||||||
@@ -80,7 +93,7 @@ class SleekTest(unittest.TestCase):
|
|||||||
Arguments:
|
Arguments:
|
||||||
xml -- An XML object to use for the Message's values.
|
xml -- An XML object to use for the Message's values.
|
||||||
"""
|
"""
|
||||||
return Message(None, *args, **kwargs)
|
return Message(self.xmpp, *args, **kwargs)
|
||||||
|
|
||||||
def Iq(self, *args, **kwargs):
|
def Iq(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -91,7 +104,7 @@ class SleekTest(unittest.TestCase):
|
|||||||
Arguments:
|
Arguments:
|
||||||
xml -- An XML object to use for the Iq's values.
|
xml -- An XML object to use for the Iq's values.
|
||||||
"""
|
"""
|
||||||
return Iq(None, *args, **kwargs)
|
return Iq(self.xmpp, *args, **kwargs)
|
||||||
|
|
||||||
def Presence(self, *args, **kwargs):
|
def Presence(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -102,7 +115,7 @@ class SleekTest(unittest.TestCase):
|
|||||||
Arguments:
|
Arguments:
|
||||||
xml -- An XML object to use for the Iq's values.
|
xml -- An XML object to use for the Iq's values.
|
||||||
"""
|
"""
|
||||||
return Presence(None, *args, **kwargs)
|
return Presence(self.xmpp, *args, **kwargs)
|
||||||
|
|
||||||
def check_jid(self, jid, user=None, domain=None, resource=None,
|
def check_jid(self, jid, user=None, domain=None, resource=None,
|
||||||
bare=None, full=None, string=None):
|
bare=None, full=None, string=None):
|
||||||
@@ -140,13 +153,12 @@ class SleekTest(unittest.TestCase):
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Methods for comparing stanza objects to XML strings
|
# Methods for comparing stanza objects to XML strings
|
||||||
|
|
||||||
def check(self, stanza, xml_string,
|
def check(self, stanza, criteria, method='exact',
|
||||||
defaults=None, use_values=True):
|
defaults=None, use_values=True):
|
||||||
"""
|
"""
|
||||||
Create and compare several stanza objects to a correct XML string.
|
Create and compare several stanza objects to a correct XML string.
|
||||||
|
|
||||||
If use_values is False, test using getStanzaValues() and
|
If use_values is False, tests using stanza.values will not be used.
|
||||||
setStanzaValues() will not be used.
|
|
||||||
|
|
||||||
Some stanzas provide default values for some interfaces, but
|
Some stanzas provide default values for some interfaces, but
|
||||||
these defaults can be problematic for testing since they can easily
|
these defaults can be problematic for testing since they can easily
|
||||||
@@ -161,74 +173,103 @@ class SleekTest(unittest.TestCase):
|
|||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
stanza -- The stanza object to test.
|
stanza -- The stanza object to test.
|
||||||
xml_string -- A string version of the correct XML expected.
|
criteria -- An expression the stanza must match against.
|
||||||
|
method -- The type of matching to use; one of:
|
||||||
|
'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
|
||||||
|
Defaults to the value of self.match_method.
|
||||||
defaults -- A list of stanza interfaces that have default
|
defaults -- A list of stanza interfaces that have default
|
||||||
values. These interfaces will be set to their
|
values. These interfaces will be set to their
|
||||||
defaults for the given and generated stanzas to
|
defaults for the given and generated stanzas to
|
||||||
prevent unexpected test failures.
|
prevent unexpected test failures.
|
||||||
use_values -- Indicates if testing using getStanzaValues() and
|
use_values -- Indicates if testing using stanza.values should
|
||||||
setStanzaValues() should be used. Defaults to
|
be used. Defaults to True.
|
||||||
True.
|
|
||||||
"""
|
"""
|
||||||
stanza_class = stanza.__class__
|
if method is None and hasattr(self, 'match_method'):
|
||||||
xml = self.parse_xml(xml_string)
|
method = getattr(self, 'match_method')
|
||||||
|
|
||||||
# Ensure that top level namespaces are used, even if they
|
if method != 'exact':
|
||||||
# were not provided.
|
matchers = {'stanzapath': StanzaPath,
|
||||||
self.fix_namespaces(stanza.xml, 'jabber:client')
|
'xpath': MatchXPath,
|
||||||
self.fix_namespaces(xml, 'jabber:client')
|
'mask': MatchXMLMask,
|
||||||
|
'id': MatcherId}
|
||||||
stanza2 = stanza_class(xml=xml)
|
Matcher = matchers.get(method, None)
|
||||||
|
if Matcher is None:
|
||||||
if use_values:
|
raise ValueError("Unknown matching method.")
|
||||||
# Using getStanzaValues() and setStanzaValues() will add
|
test = Matcher(criteria)
|
||||||
# XML for any interface that has a default value. We need
|
self.failUnless(test.match(stanza),
|
||||||
# to set those defaults on the existing stanzas and XML
|
"Stanza did not match using %s method:\n" % method + \
|
||||||
# so that they will compare correctly.
|
"Criteria:\n%s\n" % str(criteria) + \
|
||||||
default_stanza = stanza_class()
|
"Stanza:\n%s" % str(stanza))
|
||||||
if defaults is None:
|
|
||||||
known_defaults = {
|
|
||||||
Message: ['type'],
|
|
||||||
Presence: ['priority']
|
|
||||||
}
|
|
||||||
defaults = known_defaults.get(stanza_class, [])
|
|
||||||
for interface in defaults:
|
|
||||||
stanza[interface] = stanza[interface]
|
|
||||||
stanza2[interface] = stanza2[interface]
|
|
||||||
# Can really only automatically add defaults for top
|
|
||||||
# level attribute values. Anything else must be accounted
|
|
||||||
# for in the provided XML string.
|
|
||||||
if interface not in xml.attrib:
|
|
||||||
if interface in default_stanza.xml.attrib:
|
|
||||||
value = default_stanza.xml.attrib[interface]
|
|
||||||
xml.attrib[interface] = value
|
|
||||||
|
|
||||||
values = stanza2.getStanzaValues()
|
|
||||||
stanza3 = stanza_class()
|
|
||||||
stanza3.setStanzaValues(values)
|
|
||||||
|
|
||||||
debug = "Three methods for creating stanzas do not match.\n"
|
|
||||||
debug += "Given XML:\n%s\n" % tostring(xml)
|
|
||||||
debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
|
|
||||||
debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
|
|
||||||
debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
|
|
||||||
result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
|
|
||||||
else:
|
else:
|
||||||
debug = "Two methods for creating stanzas do not match.\n"
|
stanza_class = stanza.__class__
|
||||||
debug += "Given XML:\n%s\n" % tostring(xml)
|
if not isinstance(criteria, ElementBase):
|
||||||
debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
|
xml = self.parse_xml(criteria)
|
||||||
debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
|
else:
|
||||||
result = self.compare(xml, stanza.xml, stanza2.xml)
|
xml = criteria.xml
|
||||||
|
|
||||||
self.failUnless(result, debug)
|
# Ensure that top level namespaces are used, even if they
|
||||||
|
# were not provided.
|
||||||
|
self.fix_namespaces(stanza.xml, 'jabber:client')
|
||||||
|
self.fix_namespaces(xml, 'jabber:client')
|
||||||
|
|
||||||
|
stanza2 = stanza_class(xml=xml)
|
||||||
|
|
||||||
|
if use_values:
|
||||||
|
# Using stanza.values will add XML for any interface that
|
||||||
|
# has a default value. We need to set those defaults on
|
||||||
|
# the existing stanzas and XML so that they will compare
|
||||||
|
# correctly.
|
||||||
|
default_stanza = stanza_class()
|
||||||
|
if defaults is None:
|
||||||
|
known_defaults = {
|
||||||
|
Message: ['type'],
|
||||||
|
Presence: ['priority']
|
||||||
|
}
|
||||||
|
defaults = known_defaults.get(stanza_class, [])
|
||||||
|
for interface in defaults:
|
||||||
|
stanza[interface] = stanza[interface]
|
||||||
|
stanza2[interface] = stanza2[interface]
|
||||||
|
# Can really only automatically add defaults for top
|
||||||
|
# level attribute values. Anything else must be accounted
|
||||||
|
# for in the provided XML string.
|
||||||
|
if interface not in xml.attrib:
|
||||||
|
if interface in default_stanza.xml.attrib:
|
||||||
|
value = default_stanza.xml.attrib[interface]
|
||||||
|
xml.attrib[interface] = value
|
||||||
|
|
||||||
|
values = stanza2.values
|
||||||
|
stanza3 = stanza_class()
|
||||||
|
stanza3.values = values
|
||||||
|
|
||||||
|
debug = "Three methods for creating stanzas do not match.\n"
|
||||||
|
debug += "Given XML:\n%s\n" % tostring(xml)
|
||||||
|
debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
|
||||||
|
debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
|
||||||
|
debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
|
||||||
|
result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
|
||||||
|
else:
|
||||||
|
debug = "Two methods for creating stanzas do not match.\n"
|
||||||
|
debug += "Given XML:\n%s\n" % tostring(xml)
|
||||||
|
debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
|
||||||
|
debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
|
||||||
|
result = self.compare(xml, stanza.xml, stanza2.xml)
|
||||||
|
|
||||||
|
self.failUnless(result, debug)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Methods for simulating stanza streams.
|
# Methods for simulating stanza streams.
|
||||||
|
|
||||||
|
def stream_disconnect(self):
|
||||||
|
"""
|
||||||
|
Simulate a stream disconnection.
|
||||||
|
"""
|
||||||
|
if self.xmpp:
|
||||||
|
self.xmpp.socket.disconnect_error()
|
||||||
|
|
||||||
def stream_start(self, mode='client', skip=True, header=None,
|
def stream_start(self, mode='client', skip=True, header=None,
|
||||||
socket='mock', jid='tester@localhost',
|
socket='mock', jid='tester@localhost',
|
||||||
password='test', server='localhost',
|
password='test', server='localhost',
|
||||||
port=5222):
|
port=5222, plugins=None):
|
||||||
"""
|
"""
|
||||||
Initialize an XMPP client or component using a dummy XML stream.
|
Initialize an XMPP client or component using a dummy XML stream.
|
||||||
|
|
||||||
@@ -248,6 +289,8 @@ class SleekTest(unittest.TestCase):
|
|||||||
server -- The name of the XMPP server. Defaults to 'localhost'.
|
server -- The name of the XMPP server. Defaults to 'localhost'.
|
||||||
port -- The port to use when connecting to the server.
|
port -- The port to use when connecting to the server.
|
||||||
Defaults to 5222.
|
Defaults to 5222.
|
||||||
|
plugins -- List of plugins to register. By default, all plugins
|
||||||
|
are loaded.
|
||||||
"""
|
"""
|
||||||
if mode == 'client':
|
if mode == 'client':
|
||||||
self.xmpp = ClientXMPP(jid, password)
|
self.xmpp = ClientXMPP(jid, password)
|
||||||
@@ -257,6 +300,10 @@ class SleekTest(unittest.TestCase):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Unknown XMPP connection mode.")
|
raise ValueError("Unknown XMPP connection mode.")
|
||||||
|
|
||||||
|
# We will use this to wait for the session_start event
|
||||||
|
# for live connections.
|
||||||
|
skip_queue = queue.Queue()
|
||||||
|
|
||||||
if socket == 'mock':
|
if socket == 'mock':
|
||||||
self.xmpp.set_socket(TestSocket())
|
self.xmpp.set_socket(TestSocket())
|
||||||
|
|
||||||
@@ -271,17 +318,30 @@ class SleekTest(unittest.TestCase):
|
|||||||
self.xmpp.socket.recv_data(header)
|
self.xmpp.socket.recv_data(header)
|
||||||
elif socket == 'live':
|
elif socket == 'live':
|
||||||
self.xmpp.socket_class = TestLiveSocket
|
self.xmpp.socket_class = TestLiveSocket
|
||||||
|
def wait_for_session(x):
|
||||||
|
self.xmpp.socket.clear()
|
||||||
|
skip_queue.put('started')
|
||||||
|
self.xmpp.add_event_handler('session_start', wait_for_session)
|
||||||
self.xmpp.connect()
|
self.xmpp.connect()
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown socket type.")
|
raise ValueError("Unknown socket type.")
|
||||||
|
|
||||||
self.xmpp.register_plugins()
|
if plugins is None:
|
||||||
|
self.xmpp.register_plugins()
|
||||||
|
else:
|
||||||
|
for plugin in plugins:
|
||||||
|
self.xmpp.register_plugin(plugin)
|
||||||
self.xmpp.process(threaded=True)
|
self.xmpp.process(threaded=True)
|
||||||
if skip:
|
if skip:
|
||||||
# Clear startup stanzas
|
if socket != 'live':
|
||||||
self.xmpp.socket.next_sent(timeout=1)
|
# Mark send queue as usable
|
||||||
if mode == 'component':
|
self.xmpp.session_started_event.set()
|
||||||
|
# Clear startup stanzas
|
||||||
self.xmpp.socket.next_sent(timeout=1)
|
self.xmpp.socket.next_sent(timeout=1)
|
||||||
|
if mode == 'component':
|
||||||
|
self.xmpp.socket.next_sent(timeout=1)
|
||||||
|
else:
|
||||||
|
skip_queue.get(block=True, timeout=10)
|
||||||
|
|
||||||
def make_header(self, sto='',
|
def make_header(self, sto='',
|
||||||
sfrom='',
|
sfrom='',
|
||||||
@@ -320,7 +380,7 @@ class SleekTest(unittest.TestCase):
|
|||||||
parts.append('xmlns="%s"' % default_ns)
|
parts.append('xmlns="%s"' % default_ns)
|
||||||
return header % ' '.join(parts)
|
return header % ' '.join(parts)
|
||||||
|
|
||||||
def recv(self, data, stanza_class=StanzaBase, defaults=[],
|
def recv(self, data, defaults=[], method='exact',
|
||||||
use_values=True, timeout=1):
|
use_values=True, timeout=1):
|
||||||
"""
|
"""
|
||||||
Pass data to the dummy XMPP client as if it came from an XMPP server.
|
Pass data to the dummy XMPP client as if it came from an XMPP server.
|
||||||
@@ -328,15 +388,17 @@ class SleekTest(unittest.TestCase):
|
|||||||
If using a live connection, verify what the server has sent.
|
If using a live connection, verify what the server has sent.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
data -- String stanza XML to be received and processed by
|
data -- If a dummy socket is being used, the XML that is to
|
||||||
the XMPP client or component.
|
be received next. Otherwise it is the criteria used
|
||||||
stanza_class -- The stanza object class for verifying data received
|
to match against live data that is received.
|
||||||
by a live connection. Defaults to StanzaBase.
|
|
||||||
defaults -- A list of stanza interfaces with default values that
|
defaults -- A list of stanza interfaces with default values that
|
||||||
may interfere with comparisons.
|
may interfere with comparisons.
|
||||||
|
method -- Select the type of comparison to use for
|
||||||
|
verifying the received stanza. Options are 'exact',
|
||||||
|
'id', 'stanzapath', 'xpath', and 'mask'.
|
||||||
|
Defaults to the value of self.match_method.
|
||||||
use_values -- Indicates if stanza comparisons should test using
|
use_values -- Indicates if stanza comparisons should test using
|
||||||
getStanzaValues() and setStanzaValues().
|
stanza.values. Defaults to True.
|
||||||
Defaults to True.
|
|
||||||
timeout -- Time to wait in seconds for data to be received by
|
timeout -- Time to wait in seconds for data to be received by
|
||||||
a live connection.
|
a live connection.
|
||||||
"""
|
"""
|
||||||
@@ -346,11 +408,14 @@ class SleekTest(unittest.TestCase):
|
|||||||
# receiving data.
|
# receiving data.
|
||||||
recv_data = self.xmpp.socket.next_recv(timeout)
|
recv_data = self.xmpp.socket.next_recv(timeout)
|
||||||
if recv_data is None:
|
if recv_data is None:
|
||||||
return False
|
self.fail("No stanza was received.")
|
||||||
stanza = stanza_class(xml=self.parse_xml(recv_data))
|
xml = self.parse_xml(recv_data)
|
||||||
return self.check(stanza_class, stanza, data,
|
self.fix_namespaces(xml, 'jabber:client')
|
||||||
defaults=defaults,
|
stanza = self.xmpp._build_stanza(xml, 'jabber:client')
|
||||||
use_values=use_values)
|
self.check(stanza, data,
|
||||||
|
method=method,
|
||||||
|
defaults=defaults,
|
||||||
|
use_values=use_values)
|
||||||
else:
|
else:
|
||||||
# place the data in the dummy socket receiving queue.
|
# place the data in the dummy socket receiving queue.
|
||||||
data = str(data)
|
data = str(data)
|
||||||
@@ -424,21 +489,33 @@ class SleekTest(unittest.TestCase):
|
|||||||
'%s %s' % (xml.tag, xml.attrib),
|
'%s %s' % (xml.tag, xml.attrib),
|
||||||
'%s %s' % (recv_xml.tag, recv_xml.attrib)))
|
'%s %s' % (recv_xml.tag, recv_xml.attrib)))
|
||||||
|
|
||||||
def recv_feature(self, data, use_values=True, timeout=1):
|
def recv_feature(self, data, method='mask', use_values=True, timeout=1):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
if method is None and hasattr(self, 'match_method'):
|
||||||
|
method = getattr(self, 'match_method')
|
||||||
|
|
||||||
if self.xmpp.socket.is_live:
|
if self.xmpp.socket.is_live:
|
||||||
# we are working with a live connection, so we should
|
# we are working with a live connection, so we should
|
||||||
# verify what has been received instead of simulating
|
# verify what has been received instead of simulating
|
||||||
# receiving data.
|
# receiving data.
|
||||||
recv_data = self.xmpp.socket.next_recv(timeout)
|
recv_data = self.xmpp.socket.next_recv(timeout)
|
||||||
if recv_data is None:
|
|
||||||
return False
|
|
||||||
xml = self.parse_xml(data)
|
xml = self.parse_xml(data)
|
||||||
recv_xml = self.parse_xml(recv_data)
|
recv_xml = self.parse_xml(recv_data)
|
||||||
self.failUnless(self.compare(xml, recv_xml),
|
if recv_data is None:
|
||||||
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
|
self.fail("No stanza was received.")
|
||||||
tostring(xml), tostring(recv_xml)))
|
if method == 'exact':
|
||||||
|
self.failUnless(self.compare(xml, recv_xml),
|
||||||
|
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
|
||||||
|
tostring(xml), tostring(recv_xml)))
|
||||||
|
elif method == 'mask':
|
||||||
|
matcher = MatchXMLMask(xml)
|
||||||
|
self.failUnless(matcher.match(recv_xml),
|
||||||
|
"Stanza did not match using %s method:\n" % method + \
|
||||||
|
"Criteria:\n%s\n" % tostring(xml) + \
|
||||||
|
"Stanza:\n%s" % tostring(recv_xml))
|
||||||
|
else:
|
||||||
|
raise ValueError("Uknown matching method: %s" % method)
|
||||||
else:
|
else:
|
||||||
# place the data in the dummy socket receiving queue.
|
# place the data in the dummy socket receiving queue.
|
||||||
data = str(data)
|
data = str(data)
|
||||||
@@ -489,20 +566,29 @@ class SleekTest(unittest.TestCase):
|
|||||||
"Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
|
"Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
|
||||||
header, sent_header))
|
header, sent_header))
|
||||||
|
|
||||||
def send_feature(self, data, use_values=True, timeout=1):
|
def send_feature(self, data, method='mask', use_values=True, timeout=1):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
sent_data = self.xmpp.socket.next_sent(timeout)
|
sent_data = self.xmpp.socket.next_sent(timeout)
|
||||||
if sent_data is None:
|
|
||||||
return False
|
|
||||||
xml = self.parse_xml(data)
|
xml = self.parse_xml(data)
|
||||||
sent_xml = self.parse_xml(sent_data)
|
sent_xml = self.parse_xml(sent_data)
|
||||||
self.failUnless(self.compare(xml, sent_xml),
|
if sent_data is None:
|
||||||
"Features do not match.\nDesired:\n%s\nSent:\n%s" % (
|
self.fail("No stanza was sent.")
|
||||||
tostring(xml), tostring(sent_xml)))
|
if method == 'exact':
|
||||||
|
self.failUnless(self.compare(xml, sent_xml),
|
||||||
|
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
|
||||||
|
tostring(xml), tostring(sent_xml)))
|
||||||
|
elif method == 'mask':
|
||||||
|
matcher = MatchXMLMask(xml)
|
||||||
|
self.failUnless(matcher.match(sent_xml),
|
||||||
|
"Stanza did not match using %s method:\n" % method + \
|
||||||
|
"Criteria:\n%s\n" % tostring(xml) + \
|
||||||
|
"Stanza:\n%s" % tostring(sent_xml))
|
||||||
|
else:
|
||||||
|
raise ValueError("Uknown matching method: %s" % method)
|
||||||
|
|
||||||
def send(self, data, defaults=None,
|
def send(self, data, defaults=None, use_values=True,
|
||||||
use_values=True, timeout=.1):
|
timeout=.5, method='exact'):
|
||||||
"""
|
"""
|
||||||
Check that the XMPP client sent the given stanza XML.
|
Check that the XMPP client sent the given stanza XML.
|
||||||
|
|
||||||
@@ -518,15 +604,26 @@ class SleekTest(unittest.TestCase):
|
|||||||
values which may interfere with comparisons.
|
values which may interfere with comparisons.
|
||||||
timeout -- Time in seconds to wait for a stanza before
|
timeout -- Time in seconds to wait for a stanza before
|
||||||
failing the check.
|
failing the check.
|
||||||
|
method -- Select the type of comparison to use for
|
||||||
|
verifying the sent stanza. Options are 'exact',
|
||||||
|
'id', 'stanzapath', 'xpath', and 'mask'.
|
||||||
|
Defaults to the value of self.match_method.
|
||||||
"""
|
"""
|
||||||
if isinstance(data, str):
|
|
||||||
xml = self.parse_xml(data)
|
|
||||||
self.fix_namespaces(xml, 'jabber:client')
|
|
||||||
data = self.xmpp._build_stanza(xml, 'jabber:client')
|
|
||||||
sent = self.xmpp.socket.next_sent(timeout)
|
sent = self.xmpp.socket.next_sent(timeout)
|
||||||
self.check(data, sent,
|
if data is None and sent is None:
|
||||||
defaults=defaults,
|
return
|
||||||
use_values=use_values)
|
if data is None and sent is not None:
|
||||||
|
self.fail("Stanza data was sent: %s" % sent)
|
||||||
|
if sent is None:
|
||||||
|
self.fail("No stanza was sent.")
|
||||||
|
|
||||||
|
xml = self.parse_xml(sent)
|
||||||
|
self.fix_namespaces(xml, 'jabber:client')
|
||||||
|
sent = self.xmpp._build_stanza(xml, 'jabber:client')
|
||||||
|
self.check(sent, data,
|
||||||
|
method=method,
|
||||||
|
defaults=defaults,
|
||||||
|
use_values=use_values)
|
||||||
|
|
||||||
def stream_close(self):
|
def stream_close(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
4
sleekxmpp/thirdparty/__init__.py
vendored
4
sleekxmpp/thirdparty/__init__.py
vendored
@@ -0,0 +1,4 @@
|
|||||||
|
try:
|
||||||
|
from collections import OrderedDict
|
||||||
|
except:
|
||||||
|
from sleekxmpp.thirdparty.ordereddict import OrderedDict
|
||||||
|
|||||||
127
sleekxmpp/thirdparty/ordereddict.py
vendored
Normal file
127
sleekxmpp/thirdparty/ordereddict.py
vendored
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Copyright (c) 2009 Raymond Hettinger
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation files
|
||||||
|
# (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
# OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
from UserDict import DictMixin
|
||||||
|
|
||||||
|
class OrderedDict(dict, DictMixin):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
if len(args) > 1:
|
||||||
|
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||||
|
try:
|
||||||
|
self.__end
|
||||||
|
except AttributeError:
|
||||||
|
self.clear()
|
||||||
|
self.update(*args, **kwds)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.__end = end = []
|
||||||
|
end += [None, end, end] # sentinel node for doubly linked list
|
||||||
|
self.__map = {} # key --> [key, prev, next]
|
||||||
|
dict.clear(self)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
if key not in self:
|
||||||
|
end = self.__end
|
||||||
|
curr = end[1]
|
||||||
|
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
||||||
|
dict.__setitem__(self, key, value)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
dict.__delitem__(self, key)
|
||||||
|
key, prev, next = self.__map.pop(key)
|
||||||
|
prev[2] = next
|
||||||
|
next[1] = prev
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
end = self.__end
|
||||||
|
curr = end[2]
|
||||||
|
while curr is not end:
|
||||||
|
yield curr[0]
|
||||||
|
curr = curr[2]
|
||||||
|
|
||||||
|
def __reversed__(self):
|
||||||
|
end = self.__end
|
||||||
|
curr = end[1]
|
||||||
|
while curr is not end:
|
||||||
|
yield curr[0]
|
||||||
|
curr = curr[1]
|
||||||
|
|
||||||
|
def popitem(self, last=True):
|
||||||
|
if not self:
|
||||||
|
raise KeyError('dictionary is empty')
|
||||||
|
if last:
|
||||||
|
key = reversed(self).next()
|
||||||
|
else:
|
||||||
|
key = iter(self).next()
|
||||||
|
value = self.pop(key)
|
||||||
|
return key, value
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
items = [[k, self[k]] for k in self]
|
||||||
|
tmp = self.__map, self.__end
|
||||||
|
del self.__map, self.__end
|
||||||
|
inst_dict = vars(self).copy()
|
||||||
|
self.__map, self.__end = tmp
|
||||||
|
if inst_dict:
|
||||||
|
return (self.__class__, (items,), inst_dict)
|
||||||
|
return self.__class__, (items,)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return list(self)
|
||||||
|
|
||||||
|
setdefault = DictMixin.setdefault
|
||||||
|
update = DictMixin.update
|
||||||
|
pop = DictMixin.pop
|
||||||
|
values = DictMixin.values
|
||||||
|
items = DictMixin.items
|
||||||
|
iterkeys = DictMixin.iterkeys
|
||||||
|
itervalues = DictMixin.itervalues
|
||||||
|
iteritems = DictMixin.iteritems
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if not self:
|
||||||
|
return '%s()' % (self.__class__.__name__,)
|
||||||
|
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
return self.__class__(self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromkeys(cls, iterable, value=None):
|
||||||
|
d = cls()
|
||||||
|
for key in iterable:
|
||||||
|
d[key] = value
|
||||||
|
return d
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, OrderedDict):
|
||||||
|
if len(self) != len(other):
|
||||||
|
return False
|
||||||
|
for p, q in zip(self.items(), other.items()):
|
||||||
|
if p != q:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
return dict.__eq__(self, other)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self == other
|
||||||
@@ -22,6 +22,8 @@ class FileSocket(_fileobject):
|
|||||||
|
|
||||||
def read(self, size=4096):
|
def read(self, size=4096):
|
||||||
"""Read data from the socket as if it were a file."""
|
"""Read data from the socket as if it were a file."""
|
||||||
|
if self._sock is None:
|
||||||
|
return None
|
||||||
data = self._sock.recv(size)
|
data = self._sock.recv(size)
|
||||||
if data is not None:
|
if data is not None:
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ class BaseHandler(object):
|
|||||||
this handler.
|
this handler.
|
||||||
stream -- The XMLStream instance the handler should monitor.
|
stream -- The XMLStream instance the handler should monitor.
|
||||||
"""
|
"""
|
||||||
self.checkDelete = self.check_delete
|
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
self._destroy = False
|
self._destroy = False
|
||||||
@@ -87,3 +85,8 @@ class BaseHandler(object):
|
|||||||
handlers.
|
handlers.
|
||||||
"""
|
"""
|
||||||
return self._destroy
|
return self._destroy
|
||||||
|
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
BaseHandler.checkDelete = BaseHandler.check_delete
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ class Callback(BaseHandler):
|
|||||||
Arguments:
|
Arguments:
|
||||||
payload -- The matched stanza object.
|
payload -- The matched stanza object.
|
||||||
"""
|
"""
|
||||||
BaseHandler.prerun(self, payload)
|
if self._once:
|
||||||
|
self._destroy = True
|
||||||
if self._instream:
|
if self._instream:
|
||||||
self.run(payload, True)
|
self.run(payload, True)
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ class Callback(BaseHandler):
|
|||||||
Defaults to False.
|
Defaults to False.
|
||||||
"""
|
"""
|
||||||
if not self._instream or instream:
|
if not self._instream or instream:
|
||||||
BaseHandler.run(self, payload)
|
|
||||||
self._pointer(payload)
|
self._pointer(payload)
|
||||||
if self._once:
|
if self._once:
|
||||||
self._destroy = True
|
self._destroy = True
|
||||||
|
del self._pointer
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import Queue as queue
|
import Queue as queue
|
||||||
|
|
||||||
from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT
|
from sleekxmpp.xmlstream import StanzaBase
|
||||||
from sleekxmpp.xmlstream.handler.base import BaseHandler
|
from sleekxmpp.xmlstream.handler.base import BaseHandler
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ class Waiter(BaseHandler):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def wait(self, timeout=RESPONSE_TIMEOUT):
|
def wait(self, timeout=None):
|
||||||
"""
|
"""
|
||||||
Block an event handler while waiting for a stanza to arrive.
|
Block an event handler while waiting for a stanza to arrive.
|
||||||
|
|
||||||
@@ -84,6 +84,9 @@ class Waiter(BaseHandler):
|
|||||||
arrive. Defaults to the global default timeout
|
arrive. Defaults to the global default timeout
|
||||||
value sleekxmpp.xmlstream.RESPONSE_TIMEOUT.
|
value sleekxmpp.xmlstream.RESPONSE_TIMEOUT.
|
||||||
"""
|
"""
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.stream.response_timeout
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stanza = self._payload.get(True, timeout)
|
stanza = self._payload.get(True, timeout)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
See the file LICENSE for copying permission.
|
See the file LICENSE for copying permission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
class JID(object):
|
class JID(object):
|
||||||
"""
|
"""
|
||||||
@@ -42,7 +44,9 @@ class JID(object):
|
|||||||
Arguments:
|
Arguments:
|
||||||
jid - The new JID value.
|
jid - The new JID value.
|
||||||
"""
|
"""
|
||||||
self._full = self._jid = str(jid)
|
if isinstance(jid, JID):
|
||||||
|
jid = jid.full
|
||||||
|
self._full = self._jid = jid
|
||||||
self._domain = None
|
self._domain = None
|
||||||
self._resource = None
|
self._resource = None
|
||||||
self._user = None
|
self._user = None
|
||||||
@@ -71,7 +75,7 @@ class JID(object):
|
|||||||
if self._domain is None:
|
if self._domain is None:
|
||||||
self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0]
|
self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0]
|
||||||
return self._domain or ""
|
return self._domain or ""
|
||||||
elif name == 'full':
|
elif name in ('full', 'jid'):
|
||||||
return self._jid or ""
|
return self._jid or ""
|
||||||
elif name == 'bare':
|
elif name == 'bare':
|
||||||
if self._bare is None:
|
if self._bare is None:
|
||||||
@@ -121,3 +125,13 @@ class JID(object):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Use the full JID as the string value."""
|
"""Use the full JID as the string value."""
|
||||||
return self.full
|
return self.full
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.full
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""
|
||||||
|
Two JIDs are considered equal if they have the same full JID value.
|
||||||
|
"""
|
||||||
|
other = JID(other)
|
||||||
|
return self.full == other.full
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ class MatchXMLMask(MatcherBase):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# If the mask includes text, compare it.
|
# If the mask includes text, compare it.
|
||||||
if mask.text and source.text != mask.text:
|
if mask.text and source.text and \
|
||||||
|
source.text.strip() != mask.text.strip():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Compare attributes. The stanza must include the attributes
|
# Compare attributes. The stanza must include the attributes
|
||||||
@@ -127,10 +128,17 @@ class MatchXMLMask(MatcherBase):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Recursively check subelements.
|
# Recursively check subelements.
|
||||||
|
matched_elements = {}
|
||||||
for subelement in mask:
|
for subelement in mask:
|
||||||
if use_ns:
|
if use_ns:
|
||||||
if not self._mask_cmp(source.find(subelement.tag),
|
matched = False
|
||||||
subelement, use_ns):
|
for other in source.findall(subelement.tag):
|
||||||
|
matched_elements[other] = False
|
||||||
|
if self._mask_cmp(other, subelement, use_ns):
|
||||||
|
if not matched_elements.get(other, False):
|
||||||
|
matched_elements[other] = True
|
||||||
|
matched = True
|
||||||
|
if not matched:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if not self._mask_cmp(self._get_child(source, subelement.tag),
|
if not self._mask_cmp(self._get_child(source, subelement.tag),
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ class Scheduler(object):
|
|||||||
if threaded:
|
if threaded:
|
||||||
self.thread = threading.Thread(name='sheduler_process',
|
self.thread = threading.Thread(name='sheduler_process',
|
||||||
target=self._process)
|
target=self._process)
|
||||||
|
self.thread.daemon = True
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
else:
|
else:
|
||||||
self._process()
|
self._process()
|
||||||
@@ -140,7 +141,8 @@ class Scheduler(object):
|
|||||||
"""Process scheduled tasks."""
|
"""Process scheduled tasks."""
|
||||||
self.run = True
|
self.run = True
|
||||||
try:
|
try:
|
||||||
while self.run and (self.parentstop is None or not self.parentstop.isSet()):
|
while self.run and (self.parentstop is None or \
|
||||||
|
not self.parentstop.isSet()):
|
||||||
wait = 1
|
wait = 1
|
||||||
updated = False
|
updated = False
|
||||||
if self.schedule:
|
if self.schedule:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from xml.etree import cElementTree as ET
|
|||||||
|
|
||||||
from sleekxmpp.xmlstream import JID
|
from sleekxmpp.xmlstream import JID
|
||||||
from sleekxmpp.xmlstream.tostring import tostring
|
from sleekxmpp.xmlstream.tostring import tostring
|
||||||
|
from sleekxmpp.thirdparty import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -23,17 +24,32 @@ log = logging.getLogger(__name__)
|
|||||||
XML_TYPE = type(ET.Element('xml'))
|
XML_TYPE = type(ET.Element('xml'))
|
||||||
|
|
||||||
|
|
||||||
def register_stanza_plugin(stanza, plugin):
|
def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False):
|
||||||
"""
|
"""
|
||||||
Associate a stanza object as a plugin for another stanza.
|
Associate a stanza object as a plugin for another stanza.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
stanza -- The class of the parent stanza.
|
stanza -- The class of the parent stanza.
|
||||||
plugin -- The class of the plugin stanza.
|
plugin -- The class of the plugin stanza.
|
||||||
|
iterable -- Indicates if the plugin stanza should be
|
||||||
|
included in the parent stanza's iterable
|
||||||
|
'substanzas' interface results.
|
||||||
|
overrides -- Indicates if the plugin should be allowed
|
||||||
|
to override the interface handlers for
|
||||||
|
the parent stanza.
|
||||||
"""
|
"""
|
||||||
tag = "{%s}%s" % (plugin.namespace, plugin.name)
|
tag = "{%s}%s" % (plugin.namespace, plugin.name)
|
||||||
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
|
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
|
||||||
stanza.plugin_tag_map[tag] = plugin
|
stanza.plugin_tag_map[tag] = plugin
|
||||||
|
if iterable:
|
||||||
|
# Prevent weird memory reference gotchas.
|
||||||
|
stanza.plugin_iterables = stanza.plugin_iterables.copy()
|
||||||
|
stanza.plugin_iterables.add(plugin)
|
||||||
|
if overrides:
|
||||||
|
# Prevent weird memory reference gotchas.
|
||||||
|
stanza.plugin_overrides = stanza.plugin_overrides.copy()
|
||||||
|
for interface in plugin.overrides:
|
||||||
|
stanza.plugin_overrides[interface] = plugin.plugin_attrib
|
||||||
|
|
||||||
|
|
||||||
# To maintain backwards compatibility for now, preserve the camel case name.
|
# To maintain backwards compatibility for now, preserve the camel case name.
|
||||||
@@ -95,10 +111,22 @@ class ElementBase(object):
|
|||||||
>>> message['custom']['useful_thing'] = 'foo'
|
>>> message['custom']['useful_thing'] = 'foo'
|
||||||
|
|
||||||
If a plugin provides an interface that is the same as the plugin's
|
If a plugin provides an interface that is the same as the plugin's
|
||||||
plugin_attrib value, then the plugin's interface may be accessed
|
plugin_attrib value, then the plugin's interface may be assigned
|
||||||
directly from the parent stanza, as so:
|
directly from the parent stanza, as shown below, but retrieving
|
||||||
|
information will require all interfaces to be used, as so:
|
||||||
|
|
||||||
>>> message['custom'] = 'bar' # Same as using message['custom']['custom']
|
>>> message['custom'] = 'bar' # Same as using message['custom']['custom']
|
||||||
|
>>> message['custom']['custom'] # Must use all interfaces
|
||||||
|
'bar'
|
||||||
|
|
||||||
|
If the plugin sets the value is_extension = True, then both setting
|
||||||
|
and getting an interface value that is the same as the plugin's
|
||||||
|
plugin_attrib value will work, as so:
|
||||||
|
|
||||||
|
>>> message['custom'] = 'bar' # Using is_extension=True
|
||||||
|
>>> message['custom']
|
||||||
|
'bar'
|
||||||
|
|
||||||
|
|
||||||
Class Attributes:
|
Class Attributes:
|
||||||
name -- The name of the stanza's main element.
|
name -- The name of the stanza's main element.
|
||||||
@@ -108,14 +136,35 @@ class ElementBase(object):
|
|||||||
sub_interfaces -- A subset of the set of interfaces which map
|
sub_interfaces -- A subset of the set of interfaces which map
|
||||||
to subelements instead of attributes.
|
to subelements instead of attributes.
|
||||||
subitem -- A set of stanza classes which are allowed to
|
subitem -- A set of stanza classes which are allowed to
|
||||||
be added as substanzas.
|
be added as substanzas. Deprecated version
|
||||||
|
of plugin_iterables.
|
||||||
|
overrides -- A list of interfaces prepended with 'get_',
|
||||||
|
'set_', or 'del_'. If the stanza is registered
|
||||||
|
as a plugin with overrides=True, then the
|
||||||
|
parent's interface handlers will be
|
||||||
|
overridden by the plugin's matching handler.
|
||||||
types -- A set of generic type attribute values.
|
types -- A set of generic type attribute values.
|
||||||
|
tag -- The namespaced name of the stanza's root
|
||||||
|
element. Example: "{foo_ns}bar"
|
||||||
plugin_attrib -- The interface name that the stanza uses to be
|
plugin_attrib -- The interface name that the stanza uses to be
|
||||||
accessed as a plugin from another stanza.
|
accessed as a plugin from another stanza.
|
||||||
plugin_attrib_map -- A mapping of plugin attribute names with the
|
plugin_attrib_map -- A mapping of plugin attribute names with the
|
||||||
associated plugin stanza classes.
|
associated plugin stanza classes.
|
||||||
|
plugin_iterables -- A set of stanza classes which are allowed to
|
||||||
|
be added as substanzas.
|
||||||
|
plugin_overrides -- A mapping of interfaces prepended with 'get_',
|
||||||
|
'set_' or 'del_' to plugin attrib names. Allows
|
||||||
|
a plugin to override the behaviour of a parent
|
||||||
|
stanza's interface handlers.
|
||||||
plugin_tag_map -- A mapping of plugin stanza tag names with
|
plugin_tag_map -- A mapping of plugin stanza tag names with
|
||||||
the associated plugin stanza classes.
|
the associated plugin stanza classes.
|
||||||
|
is_extension -- When True, allows the stanza to provide one
|
||||||
|
additional interface to the parent stanza,
|
||||||
|
extending the interfaces supported by the
|
||||||
|
parent. Defaults to False.
|
||||||
|
xml_ns -- The XML namespace,
|
||||||
|
http://www.w3.org/XML/1998/namespace,
|
||||||
|
for use with xml:lang values.
|
||||||
|
|
||||||
Instance Attributes:
|
Instance Attributes:
|
||||||
xml -- The stanza's XML contents.
|
xml -- The stanza's XML contents.
|
||||||
@@ -125,6 +174,10 @@ class ElementBase(object):
|
|||||||
values -- A dictionary of the stanza's interfaces
|
values -- A dictionary of the stanza's interfaces
|
||||||
and interface values, including plugins.
|
and interface values, including plugins.
|
||||||
|
|
||||||
|
Class Methods
|
||||||
|
tag_name -- Return the namespaced version of the stanza's
|
||||||
|
root element's name.
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
setup -- Initialize the stanza's XML contents.
|
setup -- Initialize the stanza's XML contents.
|
||||||
enable -- Instantiate a stanza plugin.
|
enable -- Instantiate a stanza plugin.
|
||||||
@@ -144,7 +197,7 @@ class ElementBase(object):
|
|||||||
_get_attr -- Return an attribute's value from the main
|
_get_attr -- Return an attribute's value from the main
|
||||||
stanza element.
|
stanza element.
|
||||||
_get_sub_text -- Return the text contents of a subelement.
|
_get_sub_text -- Return the text contents of a subelement.
|
||||||
_set_sub_ext -- Set the text contents of a subelement.
|
_set_sub_text -- Set the text contents of a subelement.
|
||||||
_del_sub -- Remove a subelement.
|
_del_sub -- Remove a subelement.
|
||||||
match -- Compare the stanza against an XPath expression.
|
match -- Compare the stanza against an XPath expression.
|
||||||
find -- Return subelement matching an XPath expression.
|
find -- Return subelement matching an XPath expression.
|
||||||
@@ -157,6 +210,7 @@ class ElementBase(object):
|
|||||||
appendxml -- Add XML content to the stanza.
|
appendxml -- Add XML content to the stanza.
|
||||||
pop -- Remove a substanza.
|
pop -- Remove a substanza.
|
||||||
next -- Return the next iterable substanza.
|
next -- Return the next iterable substanza.
|
||||||
|
clear -- Reset the stanza's XML contents.
|
||||||
_fix_ns -- Apply the stanza's namespace to non-namespaced
|
_fix_ns -- Apply the stanza's namespace to non-namespaced
|
||||||
elements in an XPath expression.
|
elements in an XPath expression.
|
||||||
"""
|
"""
|
||||||
@@ -167,9 +221,14 @@ class ElementBase(object):
|
|||||||
interfaces = set(('type', 'to', 'from', 'id', 'payload'))
|
interfaces = set(('type', 'to', 'from', 'id', 'payload'))
|
||||||
types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat'))
|
types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat'))
|
||||||
sub_interfaces = tuple()
|
sub_interfaces = tuple()
|
||||||
|
overrides = {}
|
||||||
plugin_attrib_map = {}
|
plugin_attrib_map = {}
|
||||||
|
plugin_overrides = {}
|
||||||
|
plugin_iterables = set()
|
||||||
plugin_tag_map = {}
|
plugin_tag_map = {}
|
||||||
subitem = None
|
subitem = set()
|
||||||
|
is_extension = False
|
||||||
|
xml_ns = 'http://www.w3.org/XML/1998/namespace'
|
||||||
|
|
||||||
def __init__(self, xml=None, parent=None):
|
def __init__(self, xml=None, parent=None):
|
||||||
"""
|
"""
|
||||||
@@ -179,22 +238,11 @@ class ElementBase(object):
|
|||||||
xml -- Initialize the stanza with optional existing XML.
|
xml -- Initialize the stanza with optional existing XML.
|
||||||
parent -- Optional stanza object that contains this stanza.
|
parent -- Optional stanza object that contains this stanza.
|
||||||
"""
|
"""
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.initPlugin = self.init_plugin
|
|
||||||
self._getAttr = self._get_attr
|
|
||||||
self._setAttr = self._set_attr
|
|
||||||
self._delAttr = self._del_attr
|
|
||||||
self._getSubText = self._get_sub_text
|
|
||||||
self._setSubText = self._set_sub_text
|
|
||||||
self._delSub = self._del_sub
|
|
||||||
self.getStanzaValues = self._get_stanza_values
|
|
||||||
self.setStanzaValues = self._set_stanza_values
|
|
||||||
|
|
||||||
self.xml = xml
|
self.xml = xml
|
||||||
self.plugins = {}
|
self.plugins = OrderedDict()
|
||||||
self.iterables = []
|
self.iterables = []
|
||||||
self._index = 0
|
self._index = 0
|
||||||
|
self.tag = self.tag_name()
|
||||||
if parent is None:
|
if parent is None:
|
||||||
self.parent = None
|
self.parent = None
|
||||||
else:
|
else:
|
||||||
@@ -203,6 +251,10 @@ class ElementBase(object):
|
|||||||
ElementBase.values = property(ElementBase._get_stanza_values,
|
ElementBase.values = property(ElementBase._get_stanza_values,
|
||||||
ElementBase._set_stanza_values)
|
ElementBase._set_stanza_values)
|
||||||
|
|
||||||
|
if self.subitem is not None:
|
||||||
|
for sub in self.subitem:
|
||||||
|
self.plugin_iterables.add(sub)
|
||||||
|
|
||||||
if self.setup(xml):
|
if self.setup(xml):
|
||||||
# If we generated our own XML, then everything is ready.
|
# If we generated our own XML, then everything is ready.
|
||||||
return
|
return
|
||||||
@@ -212,11 +264,10 @@ class ElementBase(object):
|
|||||||
if child.tag in self.plugin_tag_map:
|
if child.tag in self.plugin_tag_map:
|
||||||
plugin = self.plugin_tag_map[child.tag]
|
plugin = self.plugin_tag_map[child.tag]
|
||||||
self.plugins[plugin.plugin_attrib] = plugin(child, self)
|
self.plugins[plugin.plugin_attrib] = plugin(child, self)
|
||||||
if self.subitem is not None:
|
for sub in self.plugin_iterables:
|
||||||
for sub in self.subitem:
|
if child.tag == "{%s}%s" % (sub.namespace, sub.name):
|
||||||
if child.tag == "{%s}%s" % (sub.namespace, sub.name):
|
self.iterables.append(sub(child, self))
|
||||||
self.iterables.append(sub(child, self))
|
break
|
||||||
break
|
|
||||||
|
|
||||||
def setup(self, xml=None):
|
def setup(self, xml=None):
|
||||||
"""
|
"""
|
||||||
@@ -283,14 +334,12 @@ class ElementBase(object):
|
|||||||
for interface in self.interfaces:
|
for interface in self.interfaces:
|
||||||
values[interface] = self[interface]
|
values[interface] = self[interface]
|
||||||
for plugin, stanza in self.plugins.items():
|
for plugin, stanza in self.plugins.items():
|
||||||
values[plugin] = stanza._get_stanza_values()
|
values[plugin] = stanza.values
|
||||||
if self.iterables:
|
if self.iterables:
|
||||||
iterables = []
|
iterables = []
|
||||||
for stanza in self.iterables:
|
for stanza in self.iterables:
|
||||||
iterables.append(stanza._get_stanza_values())
|
iterables.append(stanza.values)
|
||||||
iterables[-1].update({
|
iterables[-1]['__childtag__'] = stanza.tag
|
||||||
'__childtag__': "{%s}%s" % (stanza.namespace,
|
|
||||||
stanza.name)})
|
|
||||||
values['substanzas'] = iterables
|
values['substanzas'] = iterables
|
||||||
return values
|
return values
|
||||||
|
|
||||||
@@ -305,24 +354,34 @@ class ElementBase(object):
|
|||||||
Plugin interfaces may accept a nested dictionary that
|
Plugin interfaces may accept a nested dictionary that
|
||||||
will be used recursively.
|
will be used recursively.
|
||||||
"""
|
"""
|
||||||
|
iterable_interfaces = [p.plugin_attrib for \
|
||||||
|
p in self.plugin_iterables]
|
||||||
|
|
||||||
for interface, value in values.items():
|
for interface, value in values.items():
|
||||||
if interface == 'substanzas':
|
if interface == 'substanzas':
|
||||||
|
# Remove existing substanzas
|
||||||
|
for stanza in self.iterables:
|
||||||
|
self.xml.remove(stanza.xml)
|
||||||
|
self.iterables = []
|
||||||
|
|
||||||
|
# Add new substanzas
|
||||||
for subdict in value:
|
for subdict in value:
|
||||||
if '__childtag__' in subdict:
|
if '__childtag__' in subdict:
|
||||||
for subclass in self.subitem:
|
for subclass in self.plugin_iterables:
|
||||||
child_tag = "{%s}%s" % (subclass.namespace,
|
child_tag = "{%s}%s" % (subclass.namespace,
|
||||||
subclass.name)
|
subclass.name)
|
||||||
if subdict['__childtag__'] == child_tag:
|
if subdict['__childtag__'] == child_tag:
|
||||||
sub = subclass(parent=self)
|
sub = subclass(parent=self)
|
||||||
sub._set_stanza_values(subdict)
|
sub.values = subdict
|
||||||
self.iterables.append(sub)
|
self.iterables.append(sub)
|
||||||
break
|
break
|
||||||
elif interface in self.interfaces:
|
elif interface in self.interfaces:
|
||||||
self[interface] = value
|
self[interface] = value
|
||||||
elif interface in self.plugin_attrib_map:
|
elif interface in self.plugin_attrib_map:
|
||||||
if interface not in self.plugins:
|
if interface not in iterable_interfaces:
|
||||||
self.init_plugin(interface)
|
if interface not in self.plugins:
|
||||||
self.plugins[interface]._set_stanza_values(value)
|
self.init_plugin(interface)
|
||||||
|
self.plugins[interface].values = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __getitem__(self, attrib):
|
def __getitem__(self, attrib):
|
||||||
@@ -340,12 +399,13 @@ class ElementBase(object):
|
|||||||
The search order for interface value retrieval for an interface
|
The search order for interface value retrieval for an interface
|
||||||
named 'foo' is:
|
named 'foo' is:
|
||||||
1. The list of substanzas.
|
1. The list of substanzas.
|
||||||
2. The result of calling get_foo.
|
2. The result of calling the get_foo override handler.
|
||||||
3. The result of calling getFoo.
|
3. The result of calling get_foo.
|
||||||
4. The contents of the foo subelement, if foo is a sub interface.
|
4. The result of calling getFoo.
|
||||||
5. The value of the foo attribute of the XML object.
|
5. The contents of the foo subelement, if foo is a sub interface.
|
||||||
6. The plugin named 'foo'
|
6. The value of the foo attribute of the XML object.
|
||||||
7. An empty string.
|
7. The plugin named 'foo'
|
||||||
|
8. An empty string.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
attrib -- The name of the requested stanza interface.
|
attrib -- The name of the requested stanza interface.
|
||||||
@@ -355,6 +415,16 @@ class ElementBase(object):
|
|||||||
elif attrib in self.interfaces:
|
elif attrib in self.interfaces:
|
||||||
get_method = "get_%s" % attrib.lower()
|
get_method = "get_%s" % attrib.lower()
|
||||||
get_method2 = "get%s" % attrib.title()
|
get_method2 = "get%s" % attrib.title()
|
||||||
|
|
||||||
|
if self.plugin_overrides:
|
||||||
|
plugin = self.plugin_overrides.get(get_method, None)
|
||||||
|
if plugin:
|
||||||
|
if plugin not in self.plugins:
|
||||||
|
self.init_plugin(plugin)
|
||||||
|
handler = getattr(self.plugins[plugin], get_method, None)
|
||||||
|
if handler:
|
||||||
|
return handler()
|
||||||
|
|
||||||
if hasattr(self, get_method):
|
if hasattr(self, get_method):
|
||||||
return getattr(self, get_method)()
|
return getattr(self, get_method)()
|
||||||
elif hasattr(self, get_method2):
|
elif hasattr(self, get_method2):
|
||||||
@@ -367,6 +437,8 @@ class ElementBase(object):
|
|||||||
elif attrib in self.plugin_attrib_map:
|
elif attrib in self.plugin_attrib_map:
|
||||||
if attrib not in self.plugins:
|
if attrib not in self.plugins:
|
||||||
self.init_plugin(attrib)
|
self.init_plugin(attrib)
|
||||||
|
if self.plugins[attrib].is_extension:
|
||||||
|
return self.plugins[attrib][attrib]
|
||||||
return self.plugins[attrib]
|
return self.plugins[attrib]
|
||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
@@ -387,13 +459,14 @@ class ElementBase(object):
|
|||||||
The effect of interface value assignment for an interface
|
The effect of interface value assignment for an interface
|
||||||
named 'foo' will be one of:
|
named 'foo' will be one of:
|
||||||
1. Delete the interface's contents if the value is None.
|
1. Delete the interface's contents if the value is None.
|
||||||
2. Call set_foo, if it exists.
|
2. Call the set_foo override handler, if it exists.
|
||||||
3. Call setFoo, if it exists.
|
3. Call set_foo, if it exists.
|
||||||
4. Set the text of a foo element, if foo is in sub_interfaces.
|
4. Call setFoo, if it exists.
|
||||||
5. Set the value of a top level XML attribute name foo.
|
5. Set the text of a foo element, if foo is in sub_interfaces.
|
||||||
6. Attempt to pass value to a plugin named foo using the plugin's
|
6. Set the value of a top level XML attribute name foo.
|
||||||
|
7. Attempt to pass value to a plugin named foo using the plugin's
|
||||||
foo interface.
|
foo interface.
|
||||||
7. Do nothing.
|
8. Do nothing.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
attrib -- The name of the stanza interface to modify.
|
attrib -- The name of the stanza interface to modify.
|
||||||
@@ -403,6 +476,16 @@ class ElementBase(object):
|
|||||||
if value is not None:
|
if value is not None:
|
||||||
set_method = "set_%s" % attrib.lower()
|
set_method = "set_%s" % attrib.lower()
|
||||||
set_method2 = "set%s" % attrib.title()
|
set_method2 = "set%s" % attrib.title()
|
||||||
|
|
||||||
|
if self.plugin_overrides:
|
||||||
|
plugin = self.plugin_overrides.get(set_method, None)
|
||||||
|
if plugin:
|
||||||
|
if plugin not in self.plugins:
|
||||||
|
self.init_plugin(plugin)
|
||||||
|
handler = getattr(self.plugins[plugin], set_method, None)
|
||||||
|
if handler:
|
||||||
|
return handler(value)
|
||||||
|
|
||||||
if hasattr(self, set_method):
|
if hasattr(self, set_method):
|
||||||
getattr(self, set_method)(value,)
|
getattr(self, set_method)(value,)
|
||||||
elif hasattr(self, set_method2):
|
elif hasattr(self, set_method2):
|
||||||
@@ -438,12 +521,13 @@ class ElementBase(object):
|
|||||||
|
|
||||||
The effect of deleting a stanza interface value named foo will be
|
The effect of deleting a stanza interface value named foo will be
|
||||||
one of:
|
one of:
|
||||||
1. Call del_foo, if it exists.
|
1. Call del_foo override handler, if it exists.
|
||||||
2. Call delFoo, if it exists.
|
2. Call del_foo, if it exists.
|
||||||
3. Delete foo element, if foo is in sub_interfaces.
|
3. Call delFoo, if it exists.
|
||||||
4. Delete top level XML attribute named foo.
|
4. Delete foo element, if foo is in sub_interfaces.
|
||||||
5. Remove the foo plugin, if it was loaded.
|
5. Delete top level XML attribute named foo.
|
||||||
6. Do nothing.
|
6. Remove the foo plugin, if it was loaded.
|
||||||
|
7. Do nothing.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
attrib -- The name of the affected stanza interface.
|
attrib -- The name of the affected stanza interface.
|
||||||
@@ -451,6 +535,16 @@ class ElementBase(object):
|
|||||||
if attrib in self.interfaces:
|
if attrib in self.interfaces:
|
||||||
del_method = "del_%s" % attrib.lower()
|
del_method = "del_%s" % attrib.lower()
|
||||||
del_method2 = "del%s" % attrib.title()
|
del_method2 = "del%s" % attrib.title()
|
||||||
|
|
||||||
|
if self.plugin_overrides:
|
||||||
|
plugin = self.plugin_overrides.get(del_method, None)
|
||||||
|
if plugin:
|
||||||
|
if plugin not in self.plugins:
|
||||||
|
self.init_plugin(plugin)
|
||||||
|
handler = getattr(self.plugins[plugin], del_method, None)
|
||||||
|
if handler:
|
||||||
|
return handler()
|
||||||
|
|
||||||
if hasattr(self, del_method):
|
if hasattr(self, del_method):
|
||||||
getattr(self, del_method)()
|
getattr(self, del_method)()
|
||||||
elif hasattr(self, del_method2):
|
elif hasattr(self, del_method2):
|
||||||
@@ -463,8 +557,13 @@ class ElementBase(object):
|
|||||||
elif attrib in self.plugin_attrib_map:
|
elif attrib in self.plugin_attrib_map:
|
||||||
if attrib in self.plugins:
|
if attrib in self.plugins:
|
||||||
xml = self.plugins[attrib].xml
|
xml = self.plugins[attrib].xml
|
||||||
|
if self.plugins[attrib].is_extension:
|
||||||
|
del self.plugins[attrib][attrib]
|
||||||
del self.plugins[attrib]
|
del self.plugins[attrib]
|
||||||
self.xml.remove(xml)
|
try:
|
||||||
|
self.xml.remove(xml)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _set_attr(self, name, value):
|
def _set_attr(self, name, value):
|
||||||
@@ -786,6 +885,28 @@ class ElementBase(object):
|
|||||||
"""
|
"""
|
||||||
return self.__next__()
|
return self.__next__()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Remove all XML element contents and plugins.
|
||||||
|
|
||||||
|
Any attribute values will be preserved.
|
||||||
|
"""
|
||||||
|
for child in self.xml.getchildren():
|
||||||
|
self.xml.remove(child)
|
||||||
|
for plugin in list(self.plugins.keys()):
|
||||||
|
del self.plugins[plugin]
|
||||||
|
return self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tag_name(cls):
|
||||||
|
"""
|
||||||
|
Return the namespaced name of the stanza's root element.
|
||||||
|
|
||||||
|
For example, for the stanza <foo xmlns="bar" />,
|
||||||
|
stanza.tag would return "{bar}foo".
|
||||||
|
"""
|
||||||
|
return "{%s}%s" % (cls.namespace, cls.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attrib(self):
|
def attrib(self):
|
||||||
"""
|
"""
|
||||||
@@ -858,13 +979,13 @@ class ElementBase(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Check that this stanza is a superset of the other stanza.
|
# Check that this stanza is a superset of the other stanza.
|
||||||
values = self._get_stanza_values()
|
values = self.values
|
||||||
for key in other.keys():
|
for key in other.keys():
|
||||||
if key not in values or values[key] != other[key]:
|
if key not in values or values[key] != other[key]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check that the other stanza is a superset of this stanza.
|
# Check that the other stanza is a superset of this stanza.
|
||||||
values = other._get_stanza_values()
|
values = other.values
|
||||||
for key in self.keys():
|
for key in self.keys():
|
||||||
if key not in values or values[key] != self[key]:
|
if key not in values or values[key] != self[key]:
|
||||||
return False
|
return False
|
||||||
@@ -934,11 +1055,16 @@ class ElementBase(object):
|
|||||||
"""
|
"""
|
||||||
return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent)
|
return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self, top_level_ns=True):
|
||||||
"""
|
"""
|
||||||
Return a string serialization of the underlying XML object.
|
Return a string serialization of the underlying XML object.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
top_level_ns -- Display the top-most namespace.
|
||||||
|
Defaults to True.
|
||||||
"""
|
"""
|
||||||
return tostring(self.xml, xmlns='', stanza_ns=self.namespace)
|
stanza_ns = '' if top_level_ns else self.namespace
|
||||||
|
return tostring(self.xml, xmlns='', stanza_ns=stanza_ns)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""
|
"""
|
||||||
@@ -968,7 +1094,6 @@ class StanzaBase(ElementBase):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
stream -- The XMLStream instance that will handle sending this stanza.
|
stream -- The XMLStream instance that will handle sending this stanza.
|
||||||
tag -- The namespaced version of the stanza's name.
|
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
set_type -- Set the type of the stanza.
|
set_type -- Set the type of the stanza.
|
||||||
@@ -979,7 +1104,6 @@ class StanzaBase(ElementBase):
|
|||||||
get_payload -- Return the stanza's XML contents.
|
get_payload -- Return the stanza's XML contents.
|
||||||
set_payload -- Append to the stanza's XML contents.
|
set_payload -- Append to the stanza's XML contents.
|
||||||
del_payload -- Remove the stanza's XML contents.
|
del_payload -- Remove the stanza's XML contents.
|
||||||
clear -- Reset the stanza's XML contents.
|
|
||||||
reply -- Reset the stanza and modify the 'to' and 'from'
|
reply -- Reset the stanza and modify the 'to' and 'from'
|
||||||
attributes to prepare for sending a reply.
|
attributes to prepare for sending a reply.
|
||||||
error -- Set the stanza's type to 'error'.
|
error -- Set the stanza's type to 'error'.
|
||||||
@@ -1009,17 +1133,6 @@ class StanzaBase(ElementBase):
|
|||||||
sfrom -- Optional string or JID object of the sender's JID.
|
sfrom -- Optional string or JID object of the sender's JID.
|
||||||
sid -- Optional ID value for the stanza.
|
sid -- Optional ID value for the stanza.
|
||||||
"""
|
"""
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.setType = self.set_type
|
|
||||||
self.getTo = self.get_to
|
|
||||||
self.setTo = self.set_to
|
|
||||||
self.getFrom = self.get_from
|
|
||||||
self.setFrom = self.set_from
|
|
||||||
self.getPayload = self.get_payload
|
|
||||||
self.setPayload = self.set_payload
|
|
||||||
self.delPayload = self.del_payload
|
|
||||||
|
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
if stream is not None:
|
if stream is not None:
|
||||||
self.namespace = stream.default_ns
|
self.namespace = stream.default_ns
|
||||||
@@ -1094,24 +1207,17 @@ class StanzaBase(ElementBase):
|
|||||||
self.clear()
|
self.clear()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def clear(self):
|
def reply(self, clear=True):
|
||||||
"""
|
"""
|
||||||
Remove all XML element contents and plugins.
|
Swap the 'from' and 'to' attributes to prepare the stanza for
|
||||||
|
sending a reply. If clear=True, then also remove the stanza's
|
||||||
Any attribute values will be preserved.
|
contents to make room for the reply content.
|
||||||
"""
|
|
||||||
for child in self.xml.getchildren():
|
|
||||||
self.xml.remove(child)
|
|
||||||
for plugin in list(self.plugins.keys()):
|
|
||||||
del self.plugins[plugin]
|
|
||||||
return self
|
|
||||||
|
|
||||||
def reply(self):
|
|
||||||
"""
|
|
||||||
Reset the stanza and swap its 'from' and 'to' attributes to prepare
|
|
||||||
for sending a reply stanza.
|
|
||||||
|
|
||||||
For client streams, the 'from' attribute is removed.
|
For client streams, the 'from' attribute is removed.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
clear -- Indicates if the stanza's contents should be
|
||||||
|
removed. Defaults to True
|
||||||
"""
|
"""
|
||||||
# if it's a component, use from
|
# if it's a component, use from
|
||||||
if self.stream and hasattr(self.stream, "is_component") and \
|
if self.stream and hasattr(self.stream, "is_component") and \
|
||||||
@@ -1120,7 +1226,8 @@ class StanzaBase(ElementBase):
|
|||||||
else:
|
else:
|
||||||
self['to'] = self['from']
|
self['to'] = self['from']
|
||||||
del self['from']
|
del self['from']
|
||||||
self.clear()
|
if clear:
|
||||||
|
self.clear()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def error(self):
|
def error(self):
|
||||||
@@ -1146,9 +1253,15 @@ class StanzaBase(ElementBase):
|
|||||||
log.exception('Error handling {%s}%s stanza' % (self.namespace,
|
log.exception('Error handling {%s}%s stanza' % (self.namespace,
|
||||||
self.name))
|
self.name))
|
||||||
|
|
||||||
def send(self):
|
def send(self, now=False):
|
||||||
"""Queue the stanza to be sent on the XML stream."""
|
"""
|
||||||
self.stream.sendRaw(self.__str__())
|
Queue the stanza to be sent on the XML stream.
|
||||||
|
Arguments:
|
||||||
|
now -- Indicates if the queue should be skipped and the
|
||||||
|
stanza sent immediately. Useful for stream
|
||||||
|
initialization. Defaults to False.
|
||||||
|
"""
|
||||||
|
self.stream.send_raw(self.__str__(), now=now)
|
||||||
|
|
||||||
def __copy__(self):
|
def __copy__(self):
|
||||||
"""
|
"""
|
||||||
@@ -1158,8 +1271,37 @@ class StanzaBase(ElementBase):
|
|||||||
return self.__class__(xml=copy.deepcopy(self.xml),
|
return self.__class__(xml=copy.deepcopy(self.xml),
|
||||||
stream=self.stream)
|
stream=self.stream)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self, top_level_ns=False):
|
||||||
"""Serialize the stanza's XML to a string."""
|
"""
|
||||||
|
Serialize the stanza's XML to a string.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
top_level_ns -- Display the top-most namespace.
|
||||||
|
Defaults to False.
|
||||||
|
"""
|
||||||
|
stanza_ns = '' if top_level_ns else self.namespace
|
||||||
return tostring(self.xml, xmlns='',
|
return tostring(self.xml, xmlns='',
|
||||||
stanza_ns=self.namespace,
|
stanza_ns=stanza_ns,
|
||||||
stream=self.stream)
|
stream=self.stream)
|
||||||
|
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
ElementBase.initPlugin = ElementBase.init_plugin
|
||||||
|
ElementBase._getAttr = ElementBase._get_attr
|
||||||
|
ElementBase._setAttr = ElementBase._set_attr
|
||||||
|
ElementBase._delAttr = ElementBase._del_attr
|
||||||
|
ElementBase._getSubText = ElementBase._get_sub_text
|
||||||
|
ElementBase._setSubText = ElementBase._set_sub_text
|
||||||
|
ElementBase._delSub = ElementBase._del_sub
|
||||||
|
ElementBase.getStanzaValues = ElementBase._get_stanza_values
|
||||||
|
ElementBase.setStanzaValues = ElementBase._set_stanza_values
|
||||||
|
|
||||||
|
StanzaBase.setType = StanzaBase.set_type
|
||||||
|
StanzaBase.getTo = StanzaBase.get_to
|
||||||
|
StanzaBase.setTo = StanzaBase.set_to
|
||||||
|
StanzaBase.getFrom = StanzaBase.get_from
|
||||||
|
StanzaBase.setFrom = StanzaBase.set_from
|
||||||
|
StanzaBase.getPayload = StanzaBase.get_payload
|
||||||
|
StanzaBase.setPayload = StanzaBase.set_payload
|
||||||
|
StanzaBase.delPayload = StanzaBase.del_payload
|
||||||
|
|||||||
@@ -52,9 +52,18 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
|||||||
|
|
||||||
# Output escaped attribute values.
|
# Output escaped attribute values.
|
||||||
for attrib, value in xml.attrib.items():
|
for attrib, value in xml.attrib.items():
|
||||||
if '{' not in attrib:
|
value = xml_escape(value)
|
||||||
value = xml_escape(value)
|
if '}' not in attrib:
|
||||||
output.append(' %s="%s"' % (attrib, value))
|
output.append(' %s="%s"' % (attrib, value))
|
||||||
|
else:
|
||||||
|
attrib_ns = attrib.split('}')[0][1:]
|
||||||
|
attrib = attrib.split('}')[1]
|
||||||
|
if stream and attrib_ns in stream.namespace_map:
|
||||||
|
mapped_ns = stream.namespace_map[attrib_ns]
|
||||||
|
if mapped_ns:
|
||||||
|
output.append(' %s:%s="%s"' % (mapped_ns,
|
||||||
|
attrib,
|
||||||
|
value))
|
||||||
|
|
||||||
if len(xml) or xml.text:
|
if len(xml) or xml.text:
|
||||||
# If there are additional child elements to serialize.
|
# If there are additional child elements to serialize.
|
||||||
|
|||||||
@@ -55,9 +55,18 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
|
|||||||
|
|
||||||
# Output escaped attribute values.
|
# Output escaped attribute values.
|
||||||
for attrib, value in xml.attrib.items():
|
for attrib, value in xml.attrib.items():
|
||||||
if '{' not in attrib:
|
value = xml_escape(value)
|
||||||
value = xml_escape(value)
|
if '}' not in attrib:
|
||||||
output.append(u' %s="%s"' % (attrib, value))
|
output.append(' %s="%s"' % (attrib, value))
|
||||||
|
else:
|
||||||
|
attrib_ns = attrib.split('}')[0][1:]
|
||||||
|
attrib = attrib.split('}')[1]
|
||||||
|
if stream and attrib_ns in stream.namespace_map:
|
||||||
|
mapped_ns = stream.namespace_map[attrib_ns]
|
||||||
|
if mapped_ns:
|
||||||
|
output.append(' %s:%s="%s"' % (mapped_ns,
|
||||||
|
attrib,
|
||||||
|
value))
|
||||||
|
|
||||||
if len(xml) or xml.text:
|
if len(xml) or xml.text:
|
||||||
# If there are additional child elements to serialize.
|
# If there are additional child elements to serialize.
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ from __future__ import with_statement, unicode_literals
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
import signal
|
||||||
import socket as Socket
|
import socket as Socket
|
||||||
import ssl
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import types
|
import types
|
||||||
import signal
|
import random
|
||||||
try:
|
try:
|
||||||
import queue
|
import queue
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -25,6 +26,8 @@ except ImportError:
|
|||||||
from sleekxmpp.thirdparty.statemachine import StateMachine
|
from sleekxmpp.thirdparty.statemachine import StateMachine
|
||||||
from sleekxmpp.xmlstream import Scheduler, tostring
|
from sleekxmpp.xmlstream import Scheduler, tostring
|
||||||
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
||||||
|
from sleekxmpp.xmlstream.handler import Waiter, XMLCallback
|
||||||
|
from sleekxmpp.xmlstream.matcher import MatchXMLMask
|
||||||
|
|
||||||
# In Python 2.x, file socket objects are broken. A patched socket
|
# In Python 2.x, file socket objects are broken. A patched socket
|
||||||
# wrapper is provided for this case in filesocket.py.
|
# wrapper is provided for this case in filesocket.py.
|
||||||
@@ -43,6 +46,9 @@ HANDLER_THREADS = 1
|
|||||||
# Flag indicating if the SSL library is available for use.
|
# Flag indicating if the SSL library is available for use.
|
||||||
SSL_SUPPORT = True
|
SSL_SUPPORT = True
|
||||||
|
|
||||||
|
# Maximum time to delay between connection attempts is one hour.
|
||||||
|
RECONNECT_MAX_DELAY = 3600
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -92,6 +98,8 @@ class XMLStream(object):
|
|||||||
ssl_support -- Indicates if a SSL library is available for use.
|
ssl_support -- Indicates if a SSL library is available for use.
|
||||||
ssl_version -- The version of the SSL protocol to use.
|
ssl_version -- The version of the SSL protocol to use.
|
||||||
Defaults to ssl.PROTOCOL_TLSv1.
|
Defaults to ssl.PROTOCOL_TLSv1.
|
||||||
|
ca_certs -- File path to a CA certificate to verify the
|
||||||
|
server's identity.
|
||||||
state -- A state machine for managing the stream's
|
state -- A state machine for managing the stream's
|
||||||
connection state.
|
connection state.
|
||||||
stream_footer -- The start tag and any attributes for the stream's
|
stream_footer -- The start tag and any attributes for the stream's
|
||||||
@@ -100,7 +108,11 @@ class XMLStream(object):
|
|||||||
use_ssl -- Flag indicating if SSL should be used.
|
use_ssl -- Flag indicating if SSL should be used.
|
||||||
use_tls -- Flag indicating if TLS should be used.
|
use_tls -- Flag indicating if TLS should be used.
|
||||||
stop -- threading Event used to stop all threads.
|
stop -- threading Event used to stop all threads.
|
||||||
auto_reconnect-- Flag to determine whether we auto reconnect.
|
|
||||||
|
auto_reconnect -- Flag to determine whether we auto reconnect.
|
||||||
|
reconnect_max_delay -- Maximum time to delay between connection
|
||||||
|
attempts. Defaults to RECONNECT_MAX_DELAY,
|
||||||
|
which is one hour.
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
add_event_handler -- Add a handler for a custom event.
|
add_event_handler -- Add a handler for a custom event.
|
||||||
@@ -146,21 +158,13 @@ class XMLStream(object):
|
|||||||
port -- The port to use for the connection.
|
port -- The port to use for the connection.
|
||||||
Defaults to 0.
|
Defaults to 0.
|
||||||
"""
|
"""
|
||||||
# To comply with PEP8, method names now use underscores.
|
|
||||||
# Deprecated method names are re-mapped for backwards compatibility.
|
|
||||||
self.startTLS = self.start_tls
|
|
||||||
self.registerStanza = self.register_stanza
|
|
||||||
self.removeStanza = self.remove_stanza
|
|
||||||
self.registerHandler = self.register_handler
|
|
||||||
self.removeHandler = self.remove_handler
|
|
||||||
self.setSocket = self.set_socket
|
|
||||||
self.sendRaw = self.send_raw
|
|
||||||
self.getId = self.get_id
|
|
||||||
self.getNewId = self.new_id
|
|
||||||
self.sendXML = self.send_xml
|
|
||||||
|
|
||||||
self.ssl_support = SSL_SUPPORT
|
self.ssl_support = SSL_SUPPORT
|
||||||
self.ssl_version = ssl.PROTOCOL_TLSv1
|
self.ssl_version = ssl.PROTOCOL_TLSv1
|
||||||
|
self.ca_certs = None
|
||||||
|
|
||||||
|
self.response_timeout = RESPONSE_TIMEOUT
|
||||||
|
self.reconnect_delay = None
|
||||||
|
self.reconnect_max_delay = RECONNECT_MAX_DELAY
|
||||||
|
|
||||||
self.state = StateMachine(('disconnected', 'connected'))
|
self.state = StateMachine(('disconnected', 'connected'))
|
||||||
self.state._set_state('disconnected')
|
self.state._set_state('disconnected')
|
||||||
@@ -184,11 +188,14 @@ class XMLStream(object):
|
|||||||
self.stop = threading.Event()
|
self.stop = threading.Event()
|
||||||
self.stream_end_event = threading.Event()
|
self.stream_end_event = threading.Event()
|
||||||
self.stream_end_event.set()
|
self.stream_end_event.set()
|
||||||
|
self.session_started_event = threading.Event()
|
||||||
|
|
||||||
self.event_queue = queue.Queue()
|
self.event_queue = queue.Queue()
|
||||||
self.send_queue = queue.Queue()
|
self.send_queue = queue.Queue()
|
||||||
|
self.__failed_send_stanza = None
|
||||||
self.scheduler = Scheduler(self.event_queue, self.stop)
|
self.scheduler = Scheduler(self.event_queue, self.stop)
|
||||||
|
|
||||||
self.namespace_map = {}
|
self.namespace_map = {StanzaBase.xml_ns: 'xml'}
|
||||||
|
|
||||||
self.__thread = {}
|
self.__thread = {}
|
||||||
self.__root_stanza = []
|
self.__root_stanza = []
|
||||||
@@ -202,23 +209,52 @@ class XMLStream(object):
|
|||||||
self.auto_reconnect = True
|
self.auto_reconnect = True
|
||||||
self.is_client = False
|
self.is_client = False
|
||||||
|
|
||||||
|
def use_signals(self, signals=None):
|
||||||
|
"""
|
||||||
|
Register signal handlers for SIGHUP and SIGTERM, if possible,
|
||||||
|
which will raise a "killed" event when the application is
|
||||||
|
terminated.
|
||||||
|
|
||||||
|
If a signal handler already existed, it will be executed first,
|
||||||
|
before the "killed" event is raised.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
signals -- A list of signal names to be monitored.
|
||||||
|
Defaults to ['SIGHUP', 'SIGTERM'].
|
||||||
|
"""
|
||||||
|
if signals is None:
|
||||||
|
signals = ['SIGHUP', 'SIGTERM']
|
||||||
|
|
||||||
|
existing_handlers = {}
|
||||||
|
for sig_name in signals:
|
||||||
|
if hasattr(signal, sig_name):
|
||||||
|
sig = getattr(signal, sig_name)
|
||||||
|
handler = signal.getsignal(sig)
|
||||||
|
if handler:
|
||||||
|
existing_handlers[sig] = handler
|
||||||
|
|
||||||
|
def handle_kill(signum, frame):
|
||||||
|
"""
|
||||||
|
Capture kill event and disconnect cleanly after first
|
||||||
|
spawning the "killed" event.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if signum in existing_handlers and \
|
||||||
|
existing_handlers[signum] != handle_kill:
|
||||||
|
existing_handlers[signum](signum, frame)
|
||||||
|
|
||||||
|
self.event("killed", direct=True)
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(signal, 'SIGHUP'):
|
for sig_name in signals:
|
||||||
signal.signal(signal.SIGHUP, self._handle_kill)
|
if hasattr(signal, sig_name):
|
||||||
if hasattr(signal, 'SIGTERM'):
|
sig = getattr(signal, sig_name)
|
||||||
# Used in Windows
|
signal.signal(sig, handle_kill)
|
||||||
signal.signal(signal.SIGTERM, self._handle_kill)
|
self.__signals_installed = True
|
||||||
except:
|
except:
|
||||||
log.debug("Can not set interrupt signal handlers. " + \
|
log.debug("Can not set interrupt signal handlers. " + \
|
||||||
"SleekXMPP is not running from a main thread.")
|
"SleekXMPP is not running from a main thread.")
|
||||||
|
|
||||||
def _handle_kill(self, signum, frame):
|
|
||||||
"""
|
|
||||||
Capture kill event and disconnect cleanly after first
|
|
||||||
spawning the "killed" event.
|
|
||||||
"""
|
|
||||||
self.event("killed", direct=True)
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
def new_id(self):
|
def new_id(self):
|
||||||
"""
|
"""
|
||||||
@@ -277,9 +313,26 @@ class XMLStream(object):
|
|||||||
self.stop.clear()
|
self.stop.clear()
|
||||||
self.socket = self.socket_class(Socket.AF_INET, Socket.SOCK_STREAM)
|
self.socket = self.socket_class(Socket.AF_INET, Socket.SOCK_STREAM)
|
||||||
self.socket.settimeout(None)
|
self.socket.settimeout(None)
|
||||||
|
|
||||||
|
if self.reconnect_delay is None:
|
||||||
|
delay = 1.0
|
||||||
|
else:
|
||||||
|
delay = min(self.reconnect_delay * 2, self.reconnect_max_delay)
|
||||||
|
delay = random.normalvariate(delay, delay * 0.1)
|
||||||
|
log.debug('Waiting %s seconds before connecting.' % delay)
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
if self.use_ssl and self.ssl_support:
|
if self.use_ssl and self.ssl_support:
|
||||||
log.debug("Socket Wrapped for SSL")
|
log.debug("Socket Wrapped for SSL")
|
||||||
ssl_socket = ssl.wrap_socket(self.socket)
|
if self.ca_certs is None:
|
||||||
|
cert_policy = ssl.CERT_NONE
|
||||||
|
else:
|
||||||
|
cert_policy = ssl.CERT_REQUIRED
|
||||||
|
|
||||||
|
ssl_socket = ssl.wrap_socket(self.socket,
|
||||||
|
ca_certs=self.ca_certs,
|
||||||
|
cert_reqs=cert_policy)
|
||||||
|
|
||||||
if hasattr(self.socket, 'socket'):
|
if hasattr(self.socket, 'socket'):
|
||||||
# We are using a testing socket, so preserve the top
|
# We are using a testing socket, so preserve the top
|
||||||
# layer of wrapping.
|
# layer of wrapping.
|
||||||
@@ -293,12 +346,14 @@ class XMLStream(object):
|
|||||||
self.set_socket(self.socket, ignore=True)
|
self.set_socket(self.socket, ignore=True)
|
||||||
#this event is where you should set your application state
|
#this event is where you should set your application state
|
||||||
self.event("connected", direct=True)
|
self.event("connected", direct=True)
|
||||||
|
self.reconnect_delay = 1.0
|
||||||
return True
|
return True
|
||||||
except Socket.error as serr:
|
except Socket.error as serr:
|
||||||
error_msg = "Could not connect to %s:%s. Socket Error #%s: %s"
|
error_msg = "Could not connect to %s:%s. Socket Error #%s: %s"
|
||||||
|
self.event('socket_error', serr)
|
||||||
log.error(error_msg % (self.address[0], self.address[1],
|
log.error(error_msg % (self.address[0], self.address[1],
|
||||||
serr.errno, serr.strerror))
|
serr.errno, serr.strerror))
|
||||||
time.sleep(1)
|
self.reconnect_delay = delay
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def disconnect(self, reconnect=False):
|
def disconnect(self, reconnect=False):
|
||||||
@@ -318,11 +373,11 @@ class XMLStream(object):
|
|||||||
|
|
||||||
def _disconnect(self, reconnect=False):
|
def _disconnect(self, reconnect=False):
|
||||||
# Send the end of stream marker.
|
# Send the end of stream marker.
|
||||||
self.send_raw(self.stream_footer)
|
self.send_raw(self.stream_footer, now=True)
|
||||||
|
self.session_started_event.clear()
|
||||||
# Wait for confirmation that the stream was
|
# Wait for confirmation that the stream was
|
||||||
# closed in the other direction.
|
# closed in the other direction.
|
||||||
if not reconnect:
|
self.auto_reconnect = reconnect
|
||||||
self.auto_reconnect = False
|
|
||||||
self.stream_end_event.wait(4)
|
self.stream_end_event.wait(4)
|
||||||
if not self.auto_reconnect:
|
if not self.auto_reconnect:
|
||||||
self.stop.set()
|
self.stop.set()
|
||||||
@@ -331,9 +386,10 @@ class XMLStream(object):
|
|||||||
self.filesocket.close()
|
self.filesocket.close()
|
||||||
self.socket.shutdown(Socket.SHUT_RDWR)
|
self.socket.shutdown(Socket.SHUT_RDWR)
|
||||||
except Socket.error as serr:
|
except Socket.error as serr:
|
||||||
pass
|
self.event('socket_error', serr)
|
||||||
finally:
|
finally:
|
||||||
#clear your application state
|
#clear your application state
|
||||||
|
self.event('session_end', direct=True)
|
||||||
self.event("disconnected", direct=True)
|
self.event("disconnected", direct=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -383,9 +439,17 @@ class XMLStream(object):
|
|||||||
if self.ssl_support:
|
if self.ssl_support:
|
||||||
log.info("Negotiating TLS")
|
log.info("Negotiating TLS")
|
||||||
log.info("Using SSL version: %s" % str(self.ssl_version))
|
log.info("Using SSL version: %s" % str(self.ssl_version))
|
||||||
|
if self.ca_certs is None:
|
||||||
|
cert_policy = ssl.CERT_NONE
|
||||||
|
else:
|
||||||
|
cert_policy = ssl.CERT_REQUIRED
|
||||||
|
|
||||||
ssl_socket = ssl.wrap_socket(self.socket,
|
ssl_socket = ssl.wrap_socket(self.socket,
|
||||||
ssl_version=self.ssl_version,
|
ssl_version=self.ssl_version,
|
||||||
do_handshake_on_connect=False)
|
do_handshake_on_connect=False,
|
||||||
|
ca_certs=self.ca_certs,
|
||||||
|
cert_reqs=cert_policy)
|
||||||
|
|
||||||
if hasattr(self.socket, 'socket'):
|
if hasattr(self.socket, 'socket'):
|
||||||
# We are using a testing socket, so preserve the top
|
# We are using a testing socket, so preserve the top
|
||||||
# layer of wrapping.
|
# layer of wrapping.
|
||||||
@@ -458,8 +522,6 @@ class XMLStream(object):
|
|||||||
"""
|
"""
|
||||||
# To prevent circular dependencies, we must load the matcher
|
# To prevent circular dependencies, we must load the matcher
|
||||||
# and handler classes here.
|
# and handler classes here.
|
||||||
from sleekxmpp.xmlstream.matcher import MatchXMLMask
|
|
||||||
from sleekxmpp.xmlstream.handler import XMLCallback
|
|
||||||
|
|
||||||
if name is None:
|
if name is None:
|
||||||
name = 'add_handler_%s' % self.getNewId()
|
name = 'add_handler_%s' % self.getNewId()
|
||||||
@@ -606,7 +668,7 @@ class XMLStream(object):
|
|||||||
"""
|
"""
|
||||||
return xml
|
return xml
|
||||||
|
|
||||||
def send(self, data, mask=None, timeout=RESPONSE_TIMEOUT):
|
def send(self, data, mask=None, timeout=None, now=False):
|
||||||
"""
|
"""
|
||||||
A wrapper for send_raw for sending stanza objects.
|
A wrapper for send_raw for sending stanza objects.
|
||||||
|
|
||||||
@@ -620,7 +682,13 @@ class XMLStream(object):
|
|||||||
or a timeout occurs.
|
or a timeout occurs.
|
||||||
timeout -- Time in seconds to wait for a response before
|
timeout -- Time in seconds to wait for a response before
|
||||||
continuing. Defaults to RESPONSE_TIMEOUT.
|
continuing. Defaults to RESPONSE_TIMEOUT.
|
||||||
|
now -- Indicates if the send queue should be skipped,
|
||||||
|
sending the stanza immediately. Useful mainly
|
||||||
|
for stream initialization stanzas.
|
||||||
|
Defaults to False.
|
||||||
"""
|
"""
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.response_timeout
|
||||||
if hasattr(mask, 'xml'):
|
if hasattr(mask, 'xml'):
|
||||||
mask = mask.xml
|
mask = mask.xml
|
||||||
data = str(data)
|
data = str(data)
|
||||||
@@ -629,21 +697,11 @@ class XMLStream(object):
|
|||||||
wait_for = Waiter("SendWait_%s" % self.new_id(),
|
wait_for = Waiter("SendWait_%s" % self.new_id(),
|
||||||
MatchXMLMask(mask))
|
MatchXMLMask(mask))
|
||||||
self.register_handler(wait_for)
|
self.register_handler(wait_for)
|
||||||
self.send_raw(data)
|
self.send_raw(data, now)
|
||||||
if mask is not None:
|
if mask is not None:
|
||||||
return wait_for.wait(timeout)
|
return wait_for.wait(timeout)
|
||||||
|
|
||||||
def send_raw(self, data):
|
def send_xml(self, data, mask=None, timeout=None, now=False):
|
||||||
"""
|
|
||||||
Send raw data across the stream.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
data -- Any string value.
|
|
||||||
"""
|
|
||||||
self.send_queue.put(data)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def send_xml(self, data, mask=None, timeout=RESPONSE_TIMEOUT):
|
|
||||||
"""
|
"""
|
||||||
Send an XML object on the stream, and optionally wait
|
Send an XML object on the stream, and optionally wait
|
||||||
for a response.
|
for a response.
|
||||||
@@ -656,8 +714,39 @@ class XMLStream(object):
|
|||||||
or a timeout occurs.
|
or a timeout occurs.
|
||||||
timeout -- Time in seconds to wait for a response before
|
timeout -- Time in seconds to wait for a response before
|
||||||
continuing. Defaults to RESPONSE_TIMEOUT.
|
continuing. Defaults to RESPONSE_TIMEOUT.
|
||||||
|
now -- Indicates if the send queue should be skipped,
|
||||||
|
sending the stanza immediately. Useful mainly
|
||||||
|
for stream initialization stanzas.
|
||||||
|
Defaults to False.
|
||||||
"""
|
"""
|
||||||
return self.send(tostring(data), mask, timeout)
|
if timeout is None:
|
||||||
|
timeout = self.response_timeout
|
||||||
|
return self.send(tostring(data), mask, timeout, now)
|
||||||
|
|
||||||
|
def send_raw(self, data, now=False, reconnect=None):
|
||||||
|
"""
|
||||||
|
Send raw data across the stream.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
data -- Any string value.
|
||||||
|
reconnect -- Indicates if the stream should be
|
||||||
|
restarted if there is an error sending
|
||||||
|
the stanza. Used mainly for testing.
|
||||||
|
Defaults to self.auto_reconnect.
|
||||||
|
"""
|
||||||
|
if now:
|
||||||
|
log.debug("SEND (IMMED): %s" % data)
|
||||||
|
try:
|
||||||
|
self.socket.send(data.encode('utf-8'))
|
||||||
|
except Socket.error as serr:
|
||||||
|
self.event('socket_error', serr)
|
||||||
|
log.warning("Failed to send %s" % data)
|
||||||
|
if reconnect is None:
|
||||||
|
reconnect = self.auto_reconnect
|
||||||
|
self.disconnect(reconnect)
|
||||||
|
else:
|
||||||
|
self.send_queue.put(data)
|
||||||
|
return True
|
||||||
|
|
||||||
def process(self, threaded=True):
|
def process(self, threaded=True):
|
||||||
"""
|
"""
|
||||||
@@ -675,10 +764,12 @@ class XMLStream(object):
|
|||||||
Event handlers and the send queue will be threaded
|
Event handlers and the send queue will be threaded
|
||||||
regardless of this parameter's value.
|
regardless of this parameter's value.
|
||||||
"""
|
"""
|
||||||
|
self._thread_excepthook()
|
||||||
self.scheduler.process(threaded=True)
|
self.scheduler.process(threaded=True)
|
||||||
|
|
||||||
def start_thread(name, target):
|
def start_thread(name, target):
|
||||||
self.__thread[name] = threading.Thread(name=name, target=target)
|
self.__thread[name] = threading.Thread(name=name, target=target)
|
||||||
|
self.__thread[name].daemon = True
|
||||||
self.__thread[name].start()
|
self.__thread[name].start()
|
||||||
|
|
||||||
for t in range(0, HANDLER_THREADS):
|
for t in range(0, HANDLER_THREADS):
|
||||||
@@ -709,7 +800,7 @@ class XMLStream(object):
|
|||||||
firstrun = False
|
firstrun = False
|
||||||
try:
|
try:
|
||||||
if self.is_client:
|
if self.is_client:
|
||||||
self.send_raw(self.stream_header)
|
self.send_raw(self.stream_header, now=True)
|
||||||
# The call to self.__read_xml will block and prevent
|
# The call to self.__read_xml will block and prevent
|
||||||
# the body of the loop from running until a disconnect
|
# the body of the loop from running until a disconnect
|
||||||
# occurs. After any reconnection, the stream header will
|
# occurs. After any reconnection, the stream header will
|
||||||
@@ -718,14 +809,15 @@ class XMLStream(object):
|
|||||||
# Ensure the stream header is sent for any
|
# Ensure the stream header is sent for any
|
||||||
# new connections.
|
# new connections.
|
||||||
if self.is_client:
|
if self.is_client:
|
||||||
self.send_raw(self.stream_header)
|
self.send_raw(self.stream_header, now=True)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log.debug("Keyboard Escape Detected in _process")
|
log.debug("Keyboard Escape Detected in _process")
|
||||||
self.stop.set()
|
self.stop.set()
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
log.debug("SystemExit in _process")
|
log.debug("SystemExit in _process")
|
||||||
self.stop.set()
|
self.stop.set()
|
||||||
except Socket.error:
|
except Socket.error as serr:
|
||||||
|
self.event('socket_error', serr)
|
||||||
log.exception('Socket Error')
|
log.exception('Socket Error')
|
||||||
except:
|
except:
|
||||||
if not self.stop.isSet():
|
if not self.stop.isSet():
|
||||||
@@ -733,6 +825,7 @@ class XMLStream(object):
|
|||||||
if not self.stop.isSet() and self.auto_reconnect:
|
if not self.stop.isSet() and self.auto_reconnect:
|
||||||
self.reconnect()
|
self.reconnect()
|
||||||
else:
|
else:
|
||||||
|
self.event('killed', direct=True)
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
self.event_queue.put(('quit', None, None))
|
self.event_queue.put(('quit', None, None))
|
||||||
self.scheduler.run = False
|
self.scheduler.run = False
|
||||||
@@ -744,35 +837,39 @@ class XMLStream(object):
|
|||||||
"""
|
"""
|
||||||
depth = 0
|
depth = 0
|
||||||
root = None
|
root = None
|
||||||
for (event, xml) in ET.iterparse(self.filesocket, (b'end', b'start')):
|
try:
|
||||||
if event == b'start':
|
for (event, xml) in ET.iterparse(self.filesocket,
|
||||||
if depth == 0:
|
(b'end', b'start')):
|
||||||
# We have received the start of the root element.
|
if event == b'start':
|
||||||
root = xml
|
if depth == 0:
|
||||||
# Perform any stream initialization actions, such
|
# We have received the start of the root element.
|
||||||
# as handshakes.
|
root = xml
|
||||||
self.stream_end_event.clear()
|
# Perform any stream initialization actions, such
|
||||||
self.start_stream_handler(root)
|
# as handshakes.
|
||||||
depth += 1
|
self.stream_end_event.clear()
|
||||||
if event == b'end':
|
self.start_stream_handler(root)
|
||||||
depth -= 1
|
depth += 1
|
||||||
if depth == 0:
|
if event == b'end':
|
||||||
# The stream's root element has closed,
|
depth -= 1
|
||||||
# terminating the stream.
|
if depth == 0:
|
||||||
log.debug("End of stream recieved")
|
# The stream's root element has closed,
|
||||||
self.stream_end_event.set()
|
# terminating the stream.
|
||||||
return False
|
log.debug("End of stream recieved")
|
||||||
elif depth == 1:
|
self.stream_end_event.set()
|
||||||
# We only raise events for stanzas that are direct
|
return False
|
||||||
# children of the root element.
|
elif depth == 1:
|
||||||
try:
|
# We only raise events for stanzas that are direct
|
||||||
self.__spawn_event(xml)
|
# children of the root element.
|
||||||
except RestartStream:
|
try:
|
||||||
return True
|
self.__spawn_event(xml)
|
||||||
if root:
|
except RestartStream:
|
||||||
# Keep the root element empty of children to
|
return True
|
||||||
# save on memory use.
|
if root:
|
||||||
root.clear()
|
# Keep the root element empty of children to
|
||||||
|
# save on memory use.
|
||||||
|
root.clear()
|
||||||
|
except SyntaxError:
|
||||||
|
log.error("Error reading from XML stream.")
|
||||||
log.debug("Ending read XML loop")
|
log.debug("Ending read XML loop")
|
||||||
|
|
||||||
def _build_stanza(self, xml, default_ns=None):
|
def _build_stanza(self, xml, default_ns=None):
|
||||||
@@ -791,7 +888,8 @@ class XMLStream(object):
|
|||||||
default_ns = self.default_ns
|
default_ns = self.default_ns
|
||||||
stanza_type = StanzaBase
|
stanza_type = StanzaBase
|
||||||
for stanza_class in self.__root_stanza:
|
for stanza_class in self.__root_stanza:
|
||||||
if xml.tag == "{%s}%s" % (default_ns, stanza_class.name):
|
if xml.tag == "{%s}%s" % (default_ns, stanza_class.name) or \
|
||||||
|
xml.tag == stanza_class.tag_name():
|
||||||
stanza_type = stanza_class
|
stanza_type = stanza_class
|
||||||
break
|
break
|
||||||
stanza = stanza_type(self, xml)
|
stanza = stanza_type(self, xml)
|
||||||
@@ -814,12 +912,7 @@ class XMLStream(object):
|
|||||||
|
|
||||||
# Convert the raw XML object into a stanza object. If no registered
|
# Convert the raw XML object into a stanza object. If no registered
|
||||||
# stanza type applies, a generic StanzaBase stanza will be used.
|
# stanza type applies, a generic StanzaBase stanza will be used.
|
||||||
stanza_type = StanzaBase
|
stanza = self._build_stanza(xml)
|
||||||
for stanza_class in self.__root_stanza:
|
|
||||||
if xml.tag == "{%s}%s" % (self.default_ns, stanza_class.name):
|
|
||||||
stanza_type = stanza_class
|
|
||||||
break
|
|
||||||
stanza = stanza_type(self, xml)
|
|
||||||
|
|
||||||
# Match the stanza against registered handlers. Handlers marked
|
# Match the stanza against registered handlers. Handlers marked
|
||||||
# to run "in stream" will be executed immediately; the rest will
|
# to run "in stream" will be executed immediately; the rest will
|
||||||
@@ -827,12 +920,12 @@ class XMLStream(object):
|
|||||||
unhandled = True
|
unhandled = True
|
||||||
for handler in self.__handlers:
|
for handler in self.__handlers:
|
||||||
if handler.match(stanza):
|
if handler.match(stanza):
|
||||||
stanza_copy = stanza_type(self, copy.deepcopy(xml))
|
stanza_copy = copy.copy(stanza)
|
||||||
handler.prerun(stanza_copy)
|
handler.prerun(stanza_copy)
|
||||||
self.event_queue.put(('stanza', handler, stanza_copy))
|
self.event_queue.put(('stanza', handler, stanza_copy))
|
||||||
try:
|
try:
|
||||||
if handler.check_delete():
|
if handler.check_delete():
|
||||||
self.__handlers.pop(self.__handlers.index(handler))
|
self.__handlers.remove(handler)
|
||||||
except:
|
except:
|
||||||
pass # not thread safe
|
pass # not thread safe
|
||||||
unhandled = False
|
unhandled = False
|
||||||
@@ -851,13 +944,14 @@ class XMLStream(object):
|
|||||||
func -- The event handler to execute.
|
func -- The event handler to execute.
|
||||||
args -- Arguments to the event handler.
|
args -- Arguments to the event handler.
|
||||||
"""
|
"""
|
||||||
|
orig = copy.copy(args[0])
|
||||||
try:
|
try:
|
||||||
func(*args)
|
func(*args)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = 'Error processing event handler: %s'
|
error_msg = 'Error processing event handler: %s'
|
||||||
log.exception(error_msg % str(func))
|
log.exception(error_msg % str(func))
|
||||||
if hasattr(args[0], 'exception'):
|
if hasattr(orig, 'exception'):
|
||||||
args[0].exception(e)
|
orig.exception(e)
|
||||||
|
|
||||||
def _event_runner(self):
|
def _event_runner(self):
|
||||||
"""
|
"""
|
||||||
@@ -880,6 +974,7 @@ class XMLStream(object):
|
|||||||
|
|
||||||
etype, handler = event[0:2]
|
etype, handler = event[0:2]
|
||||||
args = event[2:]
|
args = event[2:]
|
||||||
|
orig = copy.copy(args[0])
|
||||||
|
|
||||||
if etype == 'stanza':
|
if etype == 'stanza':
|
||||||
try:
|
try:
|
||||||
@@ -887,15 +982,16 @@ class XMLStream(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = 'Error processing stream handler: %s'
|
error_msg = 'Error processing stream handler: %s'
|
||||||
log.exception(error_msg % handler.name)
|
log.exception(error_msg % handler.name)
|
||||||
args[0].exception(e)
|
orig.exception(e)
|
||||||
elif etype == 'schedule':
|
elif etype == 'schedule':
|
||||||
try:
|
try:
|
||||||
log.debug(args)
|
log.debug('Scheduled event: %s' % args)
|
||||||
handler(*args[0])
|
handler(*args[0])
|
||||||
except:
|
except:
|
||||||
log.exception('Error processing scheduled task')
|
log.exception('Error processing scheduled task')
|
||||||
elif etype == 'event':
|
elif etype == 'event':
|
||||||
func, threaded, disposable = handler
|
func, threaded, disposable = handler
|
||||||
|
orig = copy.copy(args[0])
|
||||||
try:
|
try:
|
||||||
if threaded:
|
if threaded:
|
||||||
x = threading.Thread(
|
x = threading.Thread(
|
||||||
@@ -908,13 +1004,14 @@ class XMLStream(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = 'Error processing event handler: %s'
|
error_msg = 'Error processing event handler: %s'
|
||||||
log.exception(error_msg % str(func))
|
log.exception(error_msg % str(func))
|
||||||
if hasattr(args[0], 'exception'):
|
if hasattr(orig, 'exception'):
|
||||||
args[0].exception(e)
|
orig.exception(e)
|
||||||
elif etype == 'quit':
|
elif etype == 'quit':
|
||||||
log.debug("Quitting event runner thread")
|
log.debug("Quitting event runner thread")
|
||||||
return False
|
return False
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log.debug("Keyboard Escape Detected in _event_runner")
|
log.debug("Keyboard Escape Detected in _event_runner")
|
||||||
|
self.event('killed', direct=True)
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
return
|
return
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
@@ -928,21 +1025,68 @@ class XMLStream(object):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
while not self.stop.isSet():
|
while not self.stop.isSet():
|
||||||
try:
|
self.session_started_event.wait()
|
||||||
data = self.send_queue.get(True, 1)
|
if self.__failed_send_stanza is not None:
|
||||||
except queue.Empty:
|
data = self.__failed_send_stanza
|
||||||
continue
|
self.__failed_send_stanza = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
data = self.send_queue.get(True, 1)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
log.debug("SEND: %s" % data)
|
log.debug("SEND: %s" % data)
|
||||||
try:
|
try:
|
||||||
self.socket.send(data.encode('utf-8'))
|
self.socket.send(data.encode('utf-8'))
|
||||||
except:
|
except Socket.error as serr:
|
||||||
|
self.event('socket_error', serr)
|
||||||
log.warning("Failed to send %s" % data)
|
log.warning("Failed to send %s" % data)
|
||||||
|
self.__failed_send_stanza = data
|
||||||
self.disconnect(self.auto_reconnect)
|
self.disconnect(self.auto_reconnect)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log.debug("Keyboard Escape Detected in _send_thread")
|
log.debug("Keyboard Escape Detected in _send_thread")
|
||||||
|
self.event('killed', direct=True)
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
return
|
return
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
self.event_queue.put(('quit', None, None))
|
self.event_queue.put(('quit', None, None))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _thread_excepthook(self):
|
||||||
|
"""
|
||||||
|
If a threaded event handler raises an exception, there is no way to
|
||||||
|
catch it except with an excepthook. Currently, each thread has its own
|
||||||
|
excepthook, but ideally we could use the main sys.excepthook.
|
||||||
|
|
||||||
|
Modifies threading.Thread to use sys.excepthook when an exception
|
||||||
|
is not caught.
|
||||||
|
"""
|
||||||
|
init_old = threading.Thread.__init__
|
||||||
|
|
||||||
|
def init(self, *args, **kwargs):
|
||||||
|
init_old(self, *args, **kwargs)
|
||||||
|
run_old = self.run
|
||||||
|
|
||||||
|
def run_with_except_hook(*args, **kw):
|
||||||
|
try:
|
||||||
|
run_old(*args, **kw)
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
self.run = run_with_except_hook
|
||||||
|
threading.Thread.__init__ = init
|
||||||
|
|
||||||
|
|
||||||
|
# To comply with PEP8, method names now use underscores.
|
||||||
|
# Deprecated method names are re-mapped for backwards compatibility.
|
||||||
|
XMLStream.startTLS = XMLStream.start_tls
|
||||||
|
XMLStream.registerStanza = XMLStream.register_stanza
|
||||||
|
XMLStream.removeStanza = XMLStream.remove_stanza
|
||||||
|
XMLStream.registerHandler = XMLStream.register_handler
|
||||||
|
XMLStream.removeHandler = XMLStream.remove_handler
|
||||||
|
XMLStream.setSocket = XMLStream.set_socket
|
||||||
|
XMLStream.sendRaw = XMLStream.send_raw
|
||||||
|
XMLStream.getId = XMLStream.get_id
|
||||||
|
XMLStream.getNewId = XMLStream.new_id
|
||||||
|
XMLStream.sendXML = XMLStream.send_xml
|
||||||
|
|||||||
57
tests/live_multiple_streams.py
Normal file
57
tests/live_multiple_streams.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from sleekxmpp.test import *
|
||||||
|
|
||||||
|
|
||||||
|
class TestMultipleStreams(SleekTest):
|
||||||
|
"""
|
||||||
|
Test that we can test a live stanza stream.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client1 = SleekTest()
|
||||||
|
self.client2 = SleekTest()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.client1.stream_close()
|
||||||
|
self.client2.stream_close()
|
||||||
|
|
||||||
|
def testMultipleStreams(self):
|
||||||
|
"""Test that we can interact with multiple live ClientXMPP instance."""
|
||||||
|
|
||||||
|
client1 = self.client1
|
||||||
|
client2 = self.client2
|
||||||
|
|
||||||
|
client1.stream_start(mode='client',
|
||||||
|
socket='live',
|
||||||
|
skip=True,
|
||||||
|
jid='user@localhost/test1',
|
||||||
|
password='user')
|
||||||
|
client2.stream_start(mode='client',
|
||||||
|
socket='live',
|
||||||
|
skip=True,
|
||||||
|
jid='user@localhost/test2',
|
||||||
|
password='user')
|
||||||
|
|
||||||
|
client1.xmpp.send_message(mto='user@localhost/test2',
|
||||||
|
mbody='test')
|
||||||
|
|
||||||
|
client1.send('message@body=test', method='stanzapath')
|
||||||
|
client2.recv('message@body=test', method='stanzapath')
|
||||||
|
|
||||||
|
|
||||||
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestMultipleStreams)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
tests = unittest.TestSuite([suite])
|
||||||
|
result = unittest.TextTestRunner(verbosity=2).run(tests)
|
||||||
|
test_ns = 'http://andyet.net/protocol/tests'
|
||||||
|
print("<tests xmlns='%s' %s %s %s %s />" % (
|
||||||
|
test_ns,
|
||||||
|
'ran="%s"' % result.testsRun,
|
||||||
|
'errors="%s"' % len(result.errors),
|
||||||
|
'fails="%s"' % len(result.failures),
|
||||||
|
'success="%s"' % result.wasSuccessful()))
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from sleekxmpp.test import *
|
from sleekxmpp.test import *
|
||||||
import sleekxmpp.plugins.xep_0033 as xep_0033
|
|
||||||
|
|
||||||
|
|
||||||
class TestLiveStream(SleekTest):
|
class TestLiveStream(SleekTest):
|
||||||
@@ -29,10 +30,6 @@ class TestLiveStream(SleekTest):
|
|||||||
<mechanism>DIGEST-MD5</mechanism>
|
<mechanism>DIGEST-MD5</mechanism>
|
||||||
<mechanism>PLAIN</mechanism>
|
<mechanism>PLAIN</mechanism>
|
||||||
</mechanisms>
|
</mechanisms>
|
||||||
<c xmlns="http://jabber.org/protocol/caps"
|
|
||||||
node="http://www.process-one.net/en/ejabberd/"
|
|
||||||
ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU=" hash="sha-1" />
|
|
||||||
<register xmlns="http://jabber.org/features/iq-register" />
|
|
||||||
</stream:features>
|
</stream:features>
|
||||||
""")
|
""")
|
||||||
self.send_feature("""
|
self.send_feature("""
|
||||||
@@ -49,11 +46,6 @@ class TestLiveStream(SleekTest):
|
|||||||
<mechanism>DIGEST-MD5</mechanism>
|
<mechanism>DIGEST-MD5</mechanism>
|
||||||
<mechanism>PLAIN</mechanism>
|
<mechanism>PLAIN</mechanism>
|
||||||
</mechanisms>
|
</mechanisms>
|
||||||
<c xmlns="http://jabber.org/protocol/caps"
|
|
||||||
node="http://www.process-one.net/en/ejabberd/"
|
|
||||||
ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU="
|
|
||||||
hash="sha-1" />
|
|
||||||
<register xmlns="http://jabber.org/features/iq-register" />
|
|
||||||
</stream:features>
|
</stream:features>
|
||||||
""")
|
""")
|
||||||
self.send_feature("""
|
self.send_feature("""
|
||||||
@@ -69,11 +61,6 @@ class TestLiveStream(SleekTest):
|
|||||||
<stream:features>
|
<stream:features>
|
||||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind" />
|
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind" />
|
||||||
<session xmlns="urn:ietf:params:xml:ns:xmpp-session" />
|
<session xmlns="urn:ietf:params:xml:ns:xmpp-session" />
|
||||||
<c xmlns="http://jabber.org/protocol/caps"
|
|
||||||
node="http://www.process-one.net/en/ejabberd/"
|
|
||||||
ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU="
|
|
||||||
hash="sha-1" />
|
|
||||||
<register xmlns="http://jabber.org/features/iq-register" />
|
|
||||||
</stream:features>
|
</stream:features>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -99,6 +86,9 @@ class TestLiveStream(SleekTest):
|
|||||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream)
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
tests = unittest.TestSuite([suite])
|
tests = unittest.TestSuite([suite])
|
||||||
result = unittest.TextTestRunner(verbosity=2).run(tests)
|
result = unittest.TextTestRunner(verbosity=2).run(tests)
|
||||||
test_ns = 'http://andyet.net/protocol/tests'
|
test_ns = 'http://andyet.net/protocol/tests'
|
||||||
|
|||||||
@@ -53,9 +53,8 @@ class TestElementBase(SleekTest):
|
|||||||
name = "foo"
|
name = "foo"
|
||||||
namespace = "foo"
|
namespace = "foo"
|
||||||
interfaces = set(('bar', 'baz'))
|
interfaces = set(('bar', 'baz'))
|
||||||
subitem = set((TestSubStanza,))
|
|
||||||
|
|
||||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
register_stanza_plugin(TestStanza, TestStanzaPlugin, iterable=True)
|
||||||
|
|
||||||
stanza = TestStanza()
|
stanza = TestStanza()
|
||||||
stanza['bar'] = 'a'
|
stanza['bar'] = 'a'
|
||||||
@@ -100,8 +99,8 @@ class TestElementBase(SleekTest):
|
|||||||
name = "foo"
|
name = "foo"
|
||||||
namespace = "foo"
|
namespace = "foo"
|
||||||
interfaces = set(('bar', 'baz'))
|
interfaces = set(('bar', 'baz'))
|
||||||
subitem = set((TestSubStanza,))
|
|
||||||
|
|
||||||
|
register_stanza_plugin(TestStanza, TestSubStanza, iterable=True)
|
||||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
||||||
register_stanza_plugin(TestStanza, TestStanzaPlugin2)
|
register_stanza_plugin(TestStanza, TestStanzaPlugin2)
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ class TestElementBase(SleekTest):
|
|||||||
'substanzas': [{'__childtag__': '{foo}subfoo',
|
'substanzas': [{'__childtag__': '{foo}subfoo',
|
||||||
'bar': 'c',
|
'bar': 'c',
|
||||||
'baz': ''}]}
|
'baz': ''}]}
|
||||||
stanza.setStanzaValues(values)
|
stanza.values = values
|
||||||
|
|
||||||
self.check(stanza, """
|
self.check(stanza, """
|
||||||
<foo xmlns="foo" bar="a">
|
<foo xmlns="foo" bar="a">
|
||||||
@@ -143,7 +142,7 @@ class TestElementBase(SleekTest):
|
|||||||
plugin_attrib = "foobar"
|
plugin_attrib = "foobar"
|
||||||
interfaces = set(('fizz',))
|
interfaces = set(('fizz',))
|
||||||
|
|
||||||
TestStanza.subitem = (TestStanza,)
|
register_stanza_plugin(TestStanza, TestStanza, iterable=True)
|
||||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
||||||
|
|
||||||
stanza = TestStanza()
|
stanza = TestStanza()
|
||||||
@@ -457,7 +456,6 @@ class TestElementBase(SleekTest):
|
|||||||
namespace = "foo"
|
namespace = "foo"
|
||||||
interfaces = set(('bar','baz', 'qux'))
|
interfaces = set(('bar','baz', 'qux'))
|
||||||
sub_interfaces = set(('qux',))
|
sub_interfaces = set(('qux',))
|
||||||
subitem = (TestSubStanza,)
|
|
||||||
|
|
||||||
def setQux(self, value):
|
def setQux(self, value):
|
||||||
self._set_sub_text('qux', text=value)
|
self._set_sub_text('qux', text=value)
|
||||||
@@ -470,6 +468,7 @@ class TestElementBase(SleekTest):
|
|||||||
namespace = "http://test/slash/bar"
|
namespace = "http://test/slash/bar"
|
||||||
interfaces = set(('attrib',))
|
interfaces = set(('attrib',))
|
||||||
|
|
||||||
|
register_stanza_plugin(TestStanza, TestSubStanza, iterable=True)
|
||||||
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
register_stanza_plugin(TestStanza, TestStanzaPlugin)
|
||||||
|
|
||||||
stanza = TestStanza()
|
stanza = TestStanza()
|
||||||
@@ -590,7 +589,8 @@ class TestElementBase(SleekTest):
|
|||||||
name = "foo"
|
name = "foo"
|
||||||
namespace = "foo"
|
namespace = "foo"
|
||||||
interfaces = set(('bar', 'baz'))
|
interfaces = set(('bar', 'baz'))
|
||||||
subitem = (TestSubStanza,)
|
|
||||||
|
register_stanza_plugin(TestStanza, TestSubStanza, iterable=True)
|
||||||
|
|
||||||
stanza = TestStanza()
|
stanza = TestStanza()
|
||||||
substanza1 = TestSubStanza()
|
substanza1 = TestSubStanza()
|
||||||
@@ -657,4 +657,87 @@ class TestElementBase(SleekTest):
|
|||||||
self.failUnless(stanza1 != stanza2,
|
self.failUnless(stanza1 != stanza2,
|
||||||
"Divergent stanza copies incorrectly compared equal.")
|
"Divergent stanza copies incorrectly compared equal.")
|
||||||
|
|
||||||
|
def testExtension(self):
|
||||||
|
"""Testing using is_extension."""
|
||||||
|
|
||||||
|
class TestStanza(ElementBase):
|
||||||
|
name = "foo"
|
||||||
|
namespace = "foo"
|
||||||
|
interfaces = set(('bar', 'baz'))
|
||||||
|
|
||||||
|
class TestExtension(ElementBase):
|
||||||
|
name = 'extended'
|
||||||
|
namespace = 'foo'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set((name,))
|
||||||
|
is_extension = True
|
||||||
|
|
||||||
|
def set_extended(self, value):
|
||||||
|
self.xml.text = value
|
||||||
|
|
||||||
|
def get_extended(self):
|
||||||
|
return self.xml.text
|
||||||
|
|
||||||
|
def del_extended(self):
|
||||||
|
self.parent().xml.remove(self.xml)
|
||||||
|
|
||||||
|
register_stanza_plugin(TestStanza, TestExtension)
|
||||||
|
|
||||||
|
stanza = TestStanza()
|
||||||
|
stanza['extended'] = 'testing'
|
||||||
|
|
||||||
|
self.check(stanza, """
|
||||||
|
<foo xmlns="foo">
|
||||||
|
<extended>testing</extended>
|
||||||
|
</foo>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.failUnless(stanza['extended'] == 'testing',
|
||||||
|
"Could not retrieve stanza extension value.")
|
||||||
|
|
||||||
|
del stanza['extended']
|
||||||
|
self.check(stanza, """
|
||||||
|
<foo xmlns="foo" />
|
||||||
|
""")
|
||||||
|
|
||||||
|
def testOverrides(self):
|
||||||
|
"""Test using interface overrides."""
|
||||||
|
|
||||||
|
class TestStanza(ElementBase):
|
||||||
|
name = "foo"
|
||||||
|
namespace = "foo"
|
||||||
|
interfaces = set(('bar', 'baz'))
|
||||||
|
|
||||||
|
class TestOverride(ElementBase):
|
||||||
|
name = 'overrider'
|
||||||
|
namespace = 'foo'
|
||||||
|
plugin_attrib = name
|
||||||
|
interfaces = set(('bar',))
|
||||||
|
overrides = ['set_bar']
|
||||||
|
|
||||||
|
def setup(self, xml):
|
||||||
|
# Don't create XML for the plugin
|
||||||
|
self.xml = ET.Element('')
|
||||||
|
|
||||||
|
def set_bar(self, value):
|
||||||
|
if not value.startswith('override-'):
|
||||||
|
self.parent()._set_attr('bar', 'override-%s' % value)
|
||||||
|
else:
|
||||||
|
self.parent()._set_attr('bar', value)
|
||||||
|
|
||||||
|
stanza = TestStanza()
|
||||||
|
stanza['bar'] = 'foo'
|
||||||
|
self.check(stanza, """
|
||||||
|
<foo xmlns="foo" bar="foo" />
|
||||||
|
""")
|
||||||
|
|
||||||
|
register_stanza_plugin(TestStanza, TestOverride, overrides=True)
|
||||||
|
|
||||||
|
stanza = TestStanza()
|
||||||
|
stanza['bar'] = 'foo'
|
||||||
|
self.check(stanza, """
|
||||||
|
<foo xmlns="foo" bar="override-foo" />
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user