Compare commits
	
		
			189 Commits
		
	
	
		
			1.0-Beta1
			...
			sleek-1.0.
		
	
	| 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 | ||
|   | 45991e47ee | ||
|   | b8f40eb843 | ||
|   | b73a859031 | ||
|   | 9dbf246f0b | ||
|   | 4fb77ac878 | ||
|   | d0c506f930 | ||
|   | 7351fe1a02 | ||
|   | 38c2f51f83 | ||
|   | 1bf34caa5b | ||
|   | 5769935720 | ||
|   | 0214db7545 | ||
|   | ffc6f031d9 | ||
|   | 9e248bb852 | ||
|   | 973890e2c9 | ||
|   | 9c08e56ed0 | ||
|   | b888610525 | ||
|   | 6d68706326 | ||
|   | 5bdcd9ef9d | ||
|   | 2eff35cc7a | ||
|   | ac330b5c6c | ||
|   | 46ffa8e9fe | ||
|   | 03847497cc | ||
|   | 185d7cf28e | ||
|   | 8aa3d0c047 | ||
|   | 9e3d506651 | ||
|   | 2f3ff37a24 | ||
|   | 1f09d60a52 | ||
|   | d528884723 | ||
|   | d9aff3d36f | ||
|   | 04cc48775d | ||
|   | 27ebb6e8f6 | ||
|   | 8f55704928 | 
| @@ -33,7 +33,7 @@ class testps(sleekxmpp.ClientXMPP): | ||||
| 		self.node = "pstestnode_%s" | ||||
| 		self.pshost = pshost | ||||
| 		if pshost is None: | ||||
| 			self.pshost = self.server | ||||
| 			self.pshost = self.boundjid.host | ||||
| 		self.nodenum = int(nodenum) | ||||
| 		self.leafnode = self.nodenum + 1 | ||||
| 		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 logging | ||||
| import time | ||||
| import getpass | ||||
| from optparse import OptionParser | ||||
|  | ||||
| import sleekxmpp | ||||
| @@ -60,8 +61,8 @@ class EchoBot(sleekxmpp.ClientXMPP): | ||||
|                      event does not provide any additional | ||||
|                      data. | ||||
|         """ | ||||
|         self.getRoster() | ||||
|         self.sendPresence() | ||||
|         self.send_presence() | ||||
|         self.get_roster() | ||||
|  | ||||
|     def message(self, msg): | ||||
|         """ | ||||
| @@ -105,14 +106,26 @@ if __name__ == '__main__': | ||||
|     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 EchoBot and register plugins. Note that while plugins may | ||||
|     # have interdependencies, the order in which you register them does | ||||
|     # not matter. | ||||
|     xmpp = EchoBot(opts.jid, opts.password) | ||||
|     xmpp.registerPlugin('xep_0030') # Service Discovery | ||||
|     xmpp.registerPlugin('xep_0004') # Data Forms | ||||
|     xmpp.registerPlugin('xep_0060') # PubSub | ||||
|     xmpp.registerPlugin('xep_0199') # XMPP Ping | ||||
|     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(): | ||||
|   | ||||
							
								
								
									
										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 | ||||
| import sys | ||||
|  | ||||
| import sleekxmpp | ||||
|  | ||||
| # if 'cygwin' in sys.platform.lower(): | ||||
| #     min_version = '0.6c6' | ||||
| # else: | ||||
| @@ -25,7 +27,7 @@ import sys | ||||
| # | ||||
| # 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).' | ||||
| LONG_DESCRIPTION = """ | ||||
| SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc). | ||||
| @@ -38,13 +40,24 @@ CLASSIFIERS      = [ 'Intended Audience :: Developers', | ||||
|                    ] | ||||
|  | ||||
| packages     = [ 'sleekxmpp', | ||||
|                  'sleekxmpp/plugins', | ||||
|                  'sleekxmpp/stanza', | ||||
|                  'sleekxmpp/test', | ||||
|                  'sleekxmpp/xmlstream', | ||||
|                  'sleekxmpp/xmlstream/matcher', | ||||
|                  'sleekxmpp/xmlstream/handler', | ||||
|                  '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): | ||||
|   | ||||
| @@ -14,3 +14,6 @@ from sleekxmpp.xmlstream.handler import * | ||||
| from sleekxmpp.xmlstream import XMLStream, RestartStream | ||||
| from sleekxmpp.xmlstream.matcher import * | ||||
| 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 | ||||
| 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.nick import Nick | ||||
| from sleekxmpp.stanza.htmlim import HTMLIM | ||||
| @@ -26,13 +26,7 @@ from sleekxmpp.xmlstream.matcher import * | ||||
| from sleekxmpp.xmlstream.handler import * | ||||
|  | ||||
|  | ||||
| # Flag indicating if DNS SRV records are available for use. | ||||
| SRV_SUPPORT = True | ||||
| try: | ||||
|     import dns.resolver | ||||
| except: | ||||
|     SRV_SUPPORT = False | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
| # In order to make sure that Unicode is handled properly | ||||
| # in Python 2.x, reset the default encoding. | ||||
| @@ -96,26 +90,14 @@ class BaseXMPP(XMLStream): | ||||
|  | ||||
|         # To comply with PEP8, method names now use underscores. | ||||
|         # 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.stream_ns = 'http://etherx.jabber.org/streams' | ||||
|  | ||||
|         self.boundjid = JID("") | ||||
|  | ||||
|         self.plugin = {} | ||||
|         self.plugin_config = {} | ||||
|         self.plugin_whitelist = [] | ||||
|         self.roster = {} | ||||
|         self.is_component = False | ||||
|         self.auto_authorize = True | ||||
| @@ -132,6 +114,10 @@ class BaseXMPP(XMLStream): | ||||
|             Callback('Presence', | ||||
|                      MatchXPath("{%s}presence" % self.default_ns), | ||||
|                      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._handle_subscribe) | ||||
| @@ -139,9 +125,10 @@ class BaseXMPP(XMLStream): | ||||
|                                self._handle_disconnected) | ||||
|  | ||||
|         # Set up the XML stream with XMPP's root stanzas. | ||||
|         self.registerStanza(Message) | ||||
|         self.registerStanza(Iq) | ||||
|         self.registerStanza(Presence) | ||||
|         self.register_stanza(Message) | ||||
|         self.register_stanza(Iq) | ||||
|         self.register_stanza(Presence) | ||||
|         self.register_stanza(StreamError) | ||||
|  | ||||
|         # Initialize a few default stanza plugins. | ||||
|         register_stanza_plugin(Iq, Roster) | ||||
| @@ -192,9 +179,9 @@ class BaseXMPP(XMLStream): | ||||
|                 xep = "(XEP-%s) " % self.plugin[plugin].xep | ||||
|  | ||||
|             desc = (xep, self.plugin[plugin].description) | ||||
|             logging.debug("Loaded Plugin %s%s" % desc) | ||||
|             log.debug("Loaded Plugin %s%s" % desc) | ||||
|         except: | ||||
|             logging.exception("Unable to load plugin: %s", plugin) | ||||
|             log.exception("Unable to load plugin: %s", plugin) | ||||
|  | ||||
|     def register_plugins(self): | ||||
|         """ | ||||
| @@ -228,7 +215,7 @@ class BaseXMPP(XMLStream): | ||||
|         if key in self.plugin: | ||||
|             return self.plugin[key] | ||||
|         else: | ||||
|             logging.warning("""Plugin "%s" is not loaded.""" % key) | ||||
|             log.warning("""Plugin "%s" is not loaded.""" % key) | ||||
|             return False | ||||
|  | ||||
|     def get(self, key, default): | ||||
| @@ -249,19 +236,27 @@ class BaseXMPP(XMLStream): | ||||
|         """Create a Presence stanza associated with this stream.""" | ||||
|         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. | ||||
|  | ||||
|         Arguments: | ||||
|             id    -- An ideally unique ID value for this stanza thread. | ||||
|                      Defaults to 0. | ||||
|             ifrom -- The from JID to use for this stanza. | ||||
|             id     -- An ideally unique ID value for this stanza thread. | ||||
|                       Defaults to 0. | ||||
|             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), | ||||
|                                              'from': ifrom}) | ||||
|         iq = self.Iq() | ||||
|         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'. | ||||
|  | ||||
| @@ -269,21 +264,45 @@ class BaseXMPP(XMLStream): | ||||
|  | ||||
|         Arguments: | ||||
|             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', | ||||
|                                              'query': queryxmlns}) | ||||
|         if not iq: | ||||
|             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. | ||||
|  | ||||
|         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, | ||||
|                                              'type': 'result'}) | ||||
|         if not iq: | ||||
|             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'. | ||||
|  | ||||
| @@ -291,15 +310,26 @@ class BaseXMPP(XMLStream): | ||||
|         stanza's payload. | ||||
|  | ||||
|         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: | ||||
|             iq.append(sub) | ||||
|         if ito: | ||||
|             iq['to'] = ito | ||||
|         if ifrom: | ||||
|             iq['from'] = ifrom | ||||
|         return iq | ||||
|  | ||||
|     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'. | ||||
|  | ||||
| @@ -310,14 +340,24 @@ class BaseXMPP(XMLStream): | ||||
|             condition -- The error condition. | ||||
|                          Defaults to 'feature-not-implemented'. | ||||
|             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}) | ||||
|         iq['error']._set_stanza_values({'type': type, | ||||
|                                         'condition': condition, | ||||
|                                         'text': text}) | ||||
|         if not iq: | ||||
|             iq = self.Iq() | ||||
|         iq['id'] = id | ||||
|         iq['error']['type'] = type | ||||
|         iq['error']['condition'] = condition | ||||
|         iq['error']['text'] = text | ||||
|         if ito: | ||||
|             iq['to'] = ito | ||||
|         if ifrom: | ||||
|             iq['from'] = ifrom | ||||
|         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 | ||||
|         query namespace. | ||||
| @@ -326,10 +366,16 @@ class BaseXMPP(XMLStream): | ||||
|             iq    -- Optional Iq stanza to modify. A new | ||||
|                      stanza is created otherwise. | ||||
|             xmlns -- The query's namespace. | ||||
|             ito   -- The destination JID for this stanza. | ||||
|             ifrom -- The from JID to use for this stanza. | ||||
|         """ | ||||
|         if not iq: | ||||
|             iq = self.Iq() | ||||
|         iq['query'] = xmlns | ||||
|         if ito: | ||||
|             iq['to'] = ito | ||||
|         if ifrom: | ||||
|             iq['from'] = ifrom | ||||
|         return iq | ||||
|  | ||||
|     def make_query_roster(self, iq=None): | ||||
| @@ -446,12 +492,12 @@ class BaseXMPP(XMLStream): | ||||
|         """ | ||||
|         Attribute accessor for bare jid | ||||
|         """ | ||||
|         logging.warning("jid property deprecated. Use boundjid.bare") | ||||
|         log.warning("jid property deprecated. Use boundjid.bare") | ||||
|         return self.boundjid.bare | ||||
|  | ||||
|     @jid.setter | ||||
|     def jid(self, value): | ||||
|         logging.warning("jid property deprecated. Use boundjid.bare") | ||||
|         log.warning("jid property deprecated. Use boundjid.bare") | ||||
|         self.boundjid.bare = value | ||||
|  | ||||
|     @property | ||||
| @@ -459,12 +505,12 @@ class BaseXMPP(XMLStream): | ||||
|         """ | ||||
|         Attribute accessor for full jid | ||||
|         """ | ||||
|         logging.warning("fulljid property deprecated. Use boundjid.full") | ||||
|         log.warning("fulljid property deprecated. Use boundjid.full") | ||||
|         return self.boundjid.full | ||||
|  | ||||
|     @fulljid.setter | ||||
|     def fulljid(self, value): | ||||
|         logging.warning("fulljid property deprecated. Use boundjid.full") | ||||
|         log.warning("fulljid property deprecated. Use boundjid.full") | ||||
|         self.boundjid.full = value | ||||
|  | ||||
|     @property | ||||
| @@ -472,12 +518,12 @@ class BaseXMPP(XMLStream): | ||||
|         """ | ||||
|         Attribute accessor for jid resource | ||||
|         """ | ||||
|         logging.warning("resource property deprecated. Use boundjid.resource") | ||||
|         log.warning("resource property deprecated. Use boundjid.resource") | ||||
|         return self.boundjid.resource | ||||
|  | ||||
|     @resource.setter | ||||
|     def resource(self, value): | ||||
|         logging.warning("fulljid property deprecated. Use boundjid.full") | ||||
|         log.warning("fulljid property deprecated. Use boundjid.full") | ||||
|         self.boundjid.resource = value | ||||
|  | ||||
|     @property | ||||
| @@ -485,12 +531,12 @@ class BaseXMPP(XMLStream): | ||||
|         """ | ||||
|         Attribute accessor for jid usernode | ||||
|         """ | ||||
|         logging.warning("username property deprecated. Use boundjid.user") | ||||
|         log.warning("username property deprecated. Use boundjid.user") | ||||
|         return self.boundjid.user | ||||
|  | ||||
|     @username.setter | ||||
|     def username(self, value): | ||||
|         logging.warning("username property deprecated. Use boundjid.user") | ||||
|         log.warning("username property deprecated. Use boundjid.user") | ||||
|         self.boundjid.user = value | ||||
|  | ||||
|     @property | ||||
| @@ -498,17 +544,17 @@ class BaseXMPP(XMLStream): | ||||
|         """ | ||||
|         Attribute accessor for jid host | ||||
|         """ | ||||
|         logging.warning("server property deprecated. Use boundjid.host") | ||||
|         log.warning("server property deprecated. Use boundjid.host") | ||||
|         return self.boundjid.server | ||||
|  | ||||
|     @server.setter | ||||
|     def server(self, value): | ||||
|         logging.warning("server property deprecated. Use boundjid.host") | ||||
|         log.warning("server property deprecated. Use boundjid.host") | ||||
|         self.boundjid.server = value | ||||
|  | ||||
|     def set_jid(self, jid): | ||||
|         """Rip a JID apart and claim it as our own.""" | ||||
|         logging.debug("setting jid to %s" % jid) | ||||
|         log.debug("setting jid to %s" % jid) | ||||
|         self.boundjid.full = jid | ||||
|  | ||||
|     def getjidresource(self, fulljid): | ||||
| @@ -524,6 +570,9 @@ class BaseXMPP(XMLStream): | ||||
|         """When disconnected, reset the roster""" | ||||
|         self.roster = {} | ||||
|  | ||||
|     def _handle_stream_error(self, error): | ||||
|         self.event('stream_error', error) | ||||
|  | ||||
|     def _handle_message(self, msg): | ||||
|         """Process incoming message stanzas.""" | ||||
|         self.event('message', msg) | ||||
| @@ -553,6 +602,7 @@ class BaseXMPP(XMLStream): | ||||
|         priority = presence['priority'] | ||||
|  | ||||
|         was_offline = False | ||||
|         got_online = False | ||||
|         old_roster = self.roster.get(jid, {}).get(resource, {}) | ||||
|  | ||||
|         # Create a new roster entry if needed. | ||||
| @@ -564,12 +614,12 @@ class BaseXMPP(XMLStream): | ||||
|                                 'in_roster': False} | ||||
|  | ||||
|         # Alias to simplify some references. | ||||
|         connections = self.roster[jid]['presence'] | ||||
|         connections = self.roster[jid].get('presence', {}) | ||||
|  | ||||
|         # Determine if the user has just come online. | ||||
|         if not resource in connections: | ||||
|             if show == 'available' or show in presence.showtypes: | ||||
|                 self.event("got_online", presence) | ||||
|                 got_online = True | ||||
|             was_offline = True | ||||
|             connections[resource] = {} | ||||
|  | ||||
| @@ -587,10 +637,11 @@ class BaseXMPP(XMLStream): | ||||
|         # disconnects. Determine if this was the last connection | ||||
|         # for the JID. | ||||
|         if show == 'unavailable': | ||||
|             logging.debug("%s %s got offline" % (jid, resource)) | ||||
|             log.debug("%s %s got offline" % (jid, 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] | ||||
|             if not was_offline: | ||||
|                 self.event("got_offline", presence) | ||||
| @@ -601,7 +652,9 @@ class BaseXMPP(XMLStream): | ||||
|  | ||||
|         # Presence state has changed. | ||||
|         self.event("changed_status", presence) | ||||
|         logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, | ||||
|         if got_online: | ||||
|             self.event("got_online", presence) | ||||
|         log.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, | ||||
|                                                    show, status)) | ||||
|  | ||||
|     def _handle_subscribe(self, presence): | ||||
| @@ -618,8 +671,8 @@ class BaseXMPP(XMLStream): | ||||
|         None       *          Disable automatic handling and use | ||||
|                               a custom handler. | ||||
|         """ | ||||
|         presence = self.Presence() | ||||
|         presence['to'] = presence['from'].bare | ||||
|         presence.reply() | ||||
|         presence['to'] = presence['to'].bare | ||||
|  | ||||
|         # We are using trinary logic, so conditions have to be | ||||
|         # more explicit than usual. | ||||
| @@ -635,3 +688,19 @@ class BaseXMPP(XMLStream): | ||||
|  | ||||
| # Restore the old, lowercased name for backwards compatibility. | ||||
| 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 | ||||
|   | ||||
| @@ -32,6 +32,9 @@ except: | ||||
|     SRV_SUPPORT = False | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class ClientXMPP(BaseXMPP): | ||||
|  | ||||
|     """ | ||||
| @@ -65,13 +68,6 @@ class ClientXMPP(BaseXMPP): | ||||
|         """ | ||||
|         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.password = password | ||||
|         self.escape_quotes = escape_quotes | ||||
| @@ -79,9 +75,6 @@ class ClientXMPP(BaseXMPP): | ||||
|         self.plugin_whitelist = plugin_whitelist | ||||
|         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.boundjid.host, | ||||
|                 "xmlns:stream='%s'" % self.stream_ns, | ||||
| @@ -133,10 +126,10 @@ class ClientXMPP(BaseXMPP): | ||||
|  | ||||
|     def _session_timeout_check(self): | ||||
|         if not self.session_started_event.isSet(): | ||||
|             logging.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) | ||||
|  | ||||
|     def connect(self, address=tuple()): | ||||
|     def connect(self, address=tuple(), reattempt=True, use_tls=True): | ||||
|         """ | ||||
|         Connect to the XMPP server. | ||||
|  | ||||
| @@ -145,25 +138,31 @@ class ClientXMPP(BaseXMPP): | ||||
|         will be used. | ||||
|  | ||||
|         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() | ||||
|         if not address or len(address) < 2: | ||||
|             if not self.srv_support: | ||||
|                 logging.debug("Did not supply (address, port) to connect" + \ | ||||
|                 log.debug("Did not supply (address, port) to connect" + \ | ||||
|                               " to and no SRV support is installed" + \ | ||||
|                               " (http://www.dnspython.org)." + \ | ||||
|                               " Continuing to attempt connection, using" + \ | ||||
|                               " server hostname from JID.") | ||||
|             else: | ||||
|                 logging.debug("Since no address is supplied," + \ | ||||
|                 log.debug("Since no address is supplied," + \ | ||||
|                               "attempting SRV lookup.") | ||||
|                 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) | ||||
|                 except dns.resolver.NXDOMAIN: | ||||
|                     logging.debug("No appropriate SRV record found." + \ | ||||
|                 except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): | ||||
|                     log.debug("No appropriate SRV record found." + \ | ||||
|                                   " Using JID server name.") | ||||
|                 except (dns.exception.Timeout,): | ||||
|                     log.debug("DNS resolution timed out.") | ||||
|                 else: | ||||
|                     # Pick a random server, weighted by priority. | ||||
|  | ||||
| @@ -187,7 +186,8 @@ class ClientXMPP(BaseXMPP): | ||||
|             # If all else fails, use the server from the JID. | ||||
|             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): | ||||
|         """ | ||||
| @@ -203,7 +203,8 @@ class ClientXMPP(BaseXMPP): | ||||
|                                          pointer, | ||||
|                                          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. | ||||
|  | ||||
| @@ -214,13 +215,25 @@ class ClientXMPP(BaseXMPP): | ||||
|                             'to', 'from', 'both', or 'none'. If set | ||||
|                             to 'remove', the entry will be deleted. | ||||
|             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, | ||||
|                                        'subscription': subscription, | ||||
|                                        'groups': groups}} | ||||
|         resp = iq.send() | ||||
|         return resp['type'] == 'result' | ||||
|         response = iq.send(block, timeout, callback) | ||||
|         if response in [False, None] or not isinstance(response, Iq): | ||||
|             return response | ||||
|         return response['type'] == 'result' | ||||
|  | ||||
|     def del_roster_item(self, jid): | ||||
|         """ | ||||
| @@ -232,11 +245,33 @@ class ClientXMPP(BaseXMPP): | ||||
|         """ | ||||
|         return self.update_roster(jid, subscription='remove') | ||||
|  | ||||
|     def get_roster(self): | ||||
|         """Request the roster from the server.""" | ||||
|         iq = self.Iq()._set_stanza_values({'type': 'get'}).enable('roster') | ||||
|         iq.send() | ||||
|         self._handle_roster(iq, request=True) | ||||
|     def get_roster(self, block=True, timeout=None, callback=None): | ||||
|         """ | ||||
|         Request the roster from the server. | ||||
|  | ||||
|         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): | ||||
|         """ | ||||
| @@ -267,16 +302,18 @@ class ClientXMPP(BaseXMPP): | ||||
|         Arguments: | ||||
|             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' | ||||
|             self.add_handler("<proceed xmlns='%s' />" % tls_ns, | ||||
|                              self._handle_tls_start, | ||||
|                              name='TLS Proceed', | ||||
|                              instream=True) | ||||
|             self.send_xml(xml) | ||||
|             self.send_xml(xml, now=True) | ||||
|             return True | ||||
|         else: | ||||
|             logging.warning("The module tlslite is required to log in" +\ | ||||
|             log.warning("The module tlslite is required to log in" +\ | ||||
|                             " to some servers, and has not been found.") | ||||
|             return False | ||||
|  | ||||
| @@ -286,7 +323,7 @@ class ClientXMPP(BaseXMPP): | ||||
|  | ||||
|         Restarts the stream. | ||||
|         """ | ||||
|         logging.debug("Starting TLS") | ||||
|         log.debug("Starting TLS") | ||||
|         if self.start_tls(): | ||||
|             raise RestartStream() | ||||
|  | ||||
| @@ -297,10 +334,11 @@ class ClientXMPP(BaseXMPP): | ||||
|         Arguments: | ||||
|             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 | ||||
|  | ||||
|         logging.debug("Starting SASL Auth") | ||||
|         log.debug("Starting SASL Auth") | ||||
|         sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl' | ||||
|         self.add_handler("<success xmlns='%s' />" % sasl_ns, | ||||
|                          self._handle_auth_success, | ||||
| @@ -328,13 +366,15 @@ class ClientXMPP(BaseXMPP): | ||||
|  | ||||
|                 self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % ( | ||||
|                     sasl_ns, | ||||
|                     auth)) | ||||
|                     auth), | ||||
|                     now=True) | ||||
|             elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user: | ||||
|                 self.send("<auth xmlns='%s' mechanism='%s' />" % ( | ||||
|                     sasl_ns, | ||||
|                     'ANONYMOUS')) | ||||
|                     'ANONYMOUS'), | ||||
|                     now=True) | ||||
|             else: | ||||
|                 logging.error("No appropriate login method.") | ||||
|                 log.error("No appropriate login method.") | ||||
|                 self.disconnect() | ||||
|         return True | ||||
|  | ||||
| @@ -356,7 +396,7 @@ class ClientXMPP(BaseXMPP): | ||||
|         Arguments: | ||||
|             xml -- The SASL authentication failure element. | ||||
|         """ | ||||
|         logging.info("Authentication failed.") | ||||
|         log.info("Authentication failed.") | ||||
|         self.event("failed_auth", direct=True) | ||||
|         self.disconnect() | ||||
|  | ||||
| @@ -367,7 +407,7 @@ class ClientXMPP(BaseXMPP): | ||||
|         Arguments: | ||||
|             xml -- The bind feature element. | ||||
|         """ | ||||
|         logging.debug("Requesting resource: %s" % self.boundjid.resource) | ||||
|         log.debug("Requesting resource: %s" % self.boundjid.resource) | ||||
|         xml.clear() | ||||
|         iq = self.Iq(stype='set') | ||||
|         if self.boundjid.resource: | ||||
| @@ -375,16 +415,16 @@ class ClientXMPP(BaseXMPP): | ||||
|             res.text = self.boundjid.resource | ||||
|             xml.append(res) | ||||
|         iq.append(xml) | ||||
|         response = iq.send() | ||||
|         response = iq.send(now=True) | ||||
|  | ||||
|         bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind' | ||||
|         self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns, | ||||
|                                                              bind_ns)).text) | ||||
|         self.bound = True | ||||
|         logging.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' | ||||
|         if "{%s}session" % session_ns not in self.features or self.bindfail: | ||||
|             logging.debug("Established Session") | ||||
|             log.debug("Established Session") | ||||
|             self.sessionstarted = True | ||||
|             self.session_started_event.set() | ||||
|             self.event("session_start") | ||||
| @@ -398,8 +438,8 @@ class ClientXMPP(BaseXMPP): | ||||
|         """ | ||||
|         if self.authenticated and self.bound: | ||||
|             iq = self.makeIqSet(xml) | ||||
|             response = iq.send() | ||||
|             logging.debug("Established Session") | ||||
|             response = iq.send(now=True) | ||||
|             log.debug("Established Session") | ||||
|             self.sessionstarted = True | ||||
|             self.session_started_event.set() | ||||
|             self.event("session_start") | ||||
| @@ -425,9 +465,19 @@ class ClientXMPP(BaseXMPP): | ||||
|                                         'presence': {}, | ||||
|                                         'in_roster': True} | ||||
|                 self.roster[jid].update(iq['roster']['items'][jid]) | ||||
|             self.event('roster_received', iq) | ||||
|  | ||||
|         self.event("roster_update", iq) | ||||
|         if iq['type'] == 'set': | ||||
|             iq.reply() | ||||
|             iq.enable('roster') | ||||
|             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 | ||||
|   | ||||
| @@ -15,13 +15,16 @@ import hashlib | ||||
|  | ||||
| from sleekxmpp import plugins | ||||
| from sleekxmpp import stanza | ||||
| from sleekxmpp.basexmpp import BaseXMPP, SRV_SUPPORT | ||||
| from sleekxmpp.basexmpp import BaseXMPP | ||||
| from sleekxmpp.xmlstream import XMLStream, RestartStream | ||||
| from sleekxmpp.xmlstream import StanzaBase, ET | ||||
| from sleekxmpp.xmlstream.matcher import * | ||||
| from sleekxmpp.xmlstream.handler import * | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class ComponentXMPP(BaseXMPP): | ||||
|  | ||||
|     """ | ||||
| @@ -67,6 +70,8 @@ class ComponentXMPP(BaseXMPP): | ||||
|         self.server_port = port | ||||
|         self.set_jid(jid) | ||||
|         self.secret = secret | ||||
|         self.plugin_config = plugin_config | ||||
|         self.plugin_whitelist = plugin_whitelist | ||||
|         self.is_component = True | ||||
|  | ||||
|         self.register_handler( | ||||
| @@ -80,7 +85,7 @@ class ComponentXMPP(BaseXMPP): | ||||
|  | ||||
|         Overrides XMLStream.connect. | ||||
|         """ | ||||
|         logging.debug("Connecting to %s:%s" % (self.server_host, | ||||
|         log.debug("Connecting to %s:%s" % (self.server_host, | ||||
|                                                self.server_port)) | ||||
|         return XMLStream.connect(self, self.server_host, | ||||
|                                        self.server_port) | ||||
| @@ -124,7 +129,7 @@ class ComponentXMPP(BaseXMPP): | ||||
|  | ||||
|         handshake = ET.Element('{jabber:component:accept}handshake') | ||||
|         handshake.text = hashlib.sha1(pre_hash).hexdigest().lower() | ||||
|         self.send_xml(handshake) | ||||
|         self.send_xml(handshake, now=True) | ||||
|  | ||||
|     def _handle_handshake(self, xml): | ||||
|         """ | ||||
| @@ -133,4 +138,5 @@ class ComponentXMPP(BaseXMPP): | ||||
|         Arguments: | ||||
|             xml -- The reply handshake stanza. | ||||
|         """ | ||||
|         self.session_started_event.set() | ||||
|         self.event("session_start") | ||||
|   | ||||
| @@ -21,7 +21,8 @@ class XMPPError(Exception): | ||||
|     """ | ||||
|  | ||||
|     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. | ||||
|  | ||||
| @@ -37,10 +38,17 @@ class XMPPError(Exception): | ||||
|             extension_args -- Content and attributes for the extension | ||||
|                               element. Same as the additional arguments to | ||||
|                               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: | ||||
|             extension_args = {} | ||||
|  | ||||
|         self.condition = condition | ||||
|         self.text = text | ||||
|         self.etype = etype | ||||
|         self.clear = clear | ||||
|         self.extension = extension | ||||
|         self.extension_ns = extension_ns | ||||
|         self.extension_args = extension_args | ||||
|   | ||||
| @@ -5,5 +5,6 @@ | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
| __all__ = ['xep_0004', 'xep_0030', 'xep_0033', 'xep_0045', 'xep_0050', | ||||
| 'xep_0078', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060'] | ||||
| __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', | ||||
|            'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086', | ||||
|            'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify'] | ||||
|   | ||||
| @@ -9,7 +9,63 @@ | ||||
|  | ||||
| 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.description = 'Base Plugin' | ||||
|         self.xmpp = xmpp | ||||
| @@ -20,7 +76,15 @@ class base_plugin(object): | ||||
|             self.plugin_init() | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         """ | ||||
|         Initialize plugin state, such as registering any stream or | ||||
|         event handlers, or new stanza types. | ||||
|         """ | ||||
|         pass | ||||
|  | ||||
|     def post_init(self): | ||||
|         """ | ||||
|         Perform any cross-plugin interactions, such as registering | ||||
|         service discovery identities or items. | ||||
|         """ | ||||
|         self.post_inited = True | ||||
|   | ||||
| @@ -14,6 +14,9 @@ from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID | ||||
| from .. stanza.iq import Iq | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class GmailQuery(ElementBase): | ||||
|     namespace = 'google:mail:notify' | ||||
|     name = 'query' | ||||
| @@ -34,12 +37,12 @@ class MailBox(ElementBase): | ||||
|     namespace = 'google:mail:notify' | ||||
|     name = 'mailbox' | ||||
|     plugin_attrib = 'mailbox' | ||||
|     interfaces = set(('result-time', 'total-matched', 'total-estimate',  | ||||
|     interfaces = set(('result-time', 'total-matched', 'total-estimate', | ||||
|                       'url', 'threads', 'matched', 'estimate')) | ||||
|  | ||||
|     def getThreads(self): | ||||
|         threads = [] | ||||
|         for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace,  | ||||
|         for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace, | ||||
|                                                       MailThread.name)): | ||||
|             threads.append(MailThread(xml=threadXML, parent=None)) | ||||
|         return threads | ||||
| @@ -55,10 +58,10 @@ class MailThread(ElementBase): | ||||
|     namespace = 'google:mail:notify' | ||||
|     name = 'mail-thread-info' | ||||
|     plugin_attrib = 'thread' | ||||
|     interfaces = set(('tid', 'participation', 'messages', 'date',  | ||||
|     interfaces = set(('tid', 'participation', 'messages', 'date', | ||||
|                       'senders', 'url', 'labels', 'subject', 'snippet')) | ||||
|     sub_interfaces = set(('labels', 'subject', 'snippet')) | ||||
|          | ||||
|  | ||||
|     def getSenders(self): | ||||
|         senders = [] | ||||
|         sendersXML = self.xml.find('{%s}senders' % self.namespace) | ||||
| @@ -91,13 +94,13 @@ class gmail_notify(base.base_plugin): | ||||
|     """ | ||||
|     Google Talk: Gmail Notifications | ||||
|     """ | ||||
|      | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         self.description = 'Google Talk: Gmail Notifications' | ||||
|  | ||||
|         self.xmpp.registerHandler( | ||||
|             Callback('Gmail Result', | ||||
|                      MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,  | ||||
|                      MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns, | ||||
|                                                    MailBox.namespace, | ||||
|                                                    MailBox.name)), | ||||
|                      self.handle_gmail)) | ||||
| @@ -108,7 +111,7 @@ class gmail_notify(base.base_plugin): | ||||
|                                                    NewMail.namespace, | ||||
|                                                    NewMail.name)), | ||||
|                      self.handle_new_mail)) | ||||
|          | ||||
|  | ||||
|         registerStanzaPlugin(Iq, GmailQuery) | ||||
|         registerStanzaPlugin(Iq, MailBox) | ||||
|         registerStanzaPlugin(Iq, NewMail) | ||||
| @@ -118,12 +121,12 @@ class gmail_notify(base.base_plugin): | ||||
|     def handle_gmail(self, iq): | ||||
|         mailbox = iq['mailbox'] | ||||
|         approx = ' approximately' if mailbox['estimated'] else '' | ||||
|         logging.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched'])) | ||||
|         log.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched'])) | ||||
|         self.last_result_time = mailbox['result-time'] | ||||
|         self.xmpp.event('gmail_messages', iq) | ||||
|  | ||||
|     def handle_new_mail(self, iq): | ||||
|         logging.info("Gmail: New emails received!") | ||||
|         log.info("Gmail: New emails received!") | ||||
|         self.xmpp.event('gmail_notify') | ||||
|         self.checkEmail() | ||||
|  | ||||
| @@ -135,12 +138,12 @@ class gmail_notify(base.base_plugin): | ||||
|  | ||||
|     def search(self, query=None, newer=None): | ||||
|         if query is None: | ||||
|             logging.info("Gmail: Checking for new emails") | ||||
|             log.info("Gmail: Checking for new emails") | ||||
|         else: | ||||
|             logging.info('Gmail: Searching for emails matching: "%s"' % query) | ||||
|             log.info('Gmail: Searching for emails matching: "%s"' % query) | ||||
|         iq = self.xmpp.Iq() | ||||
|         iq['type'] = 'get' | ||||
|         iq['to'] = self.xmpp.jid | ||||
|         iq['to'] = self.xmpp.boundjid.bare | ||||
|         iq['gmail']['q'] = query | ||||
|         iq['gmail']['newer-than-time'] = newer | ||||
|         return iq.send() | ||||
|   | ||||
| @@ -1,17 +1,20 @@ | ||||
| from . import base | ||||
| import logging | ||||
| from xml.etree import cElementTree as ET | ||||
| import types | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class jobs(base.base_plugin): | ||||
| 	def plugin_init(self): | ||||
| 		self.xep = 'pubsubjob' | ||||
| 		self.description = "Job distribution over Pubsub" | ||||
| 	 | ||||
|  | ||||
| 	def post_init(self): | ||||
| 		pass | ||||
| 		#TODO add event | ||||
| 	 | ||||
|  | ||||
| 	def createJobNode(self, host, jid, node, config=None): | ||||
| 		pass | ||||
|  | ||||
| @@ -39,8 +42,8 @@ class jobs(base.base_plugin): | ||||
| 		iq['psstate']['item'] = jobid | ||||
| 		iq['psstate']['payload'] = state | ||||
| 		result = iq.send() | ||||
| 		if result is None or type(result) == types.BooleanType or result['type'] != 'result': | ||||
| 			logging.error("Unable to change %s:%s to %s" % (node, jobid, state)) | ||||
| 		if result is None or type(result) == bool or result['type'] != 'result': | ||||
| 			log.error("Unable to change %s:%s to %s" % (node, jobid, state)) | ||||
| 			return False | ||||
| 		return True | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|     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 . import base | ||||
| @@ -12,32 +12,36 @@ import copy | ||||
| import logging | ||||
| #TODO support item groups and results | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class old_0004(base.base_plugin): | ||||
| 	 | ||||
|  | ||||
| 	def plugin_init(self): | ||||
| 		self.xep = '0004' | ||||
| 		self.description = '*Deprecated Data Forms' | ||||
| 		self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform, name='Old Message Form') | ||||
| 	 | ||||
|  | ||||
| 	def post_init(self): | ||||
| 		base.base_plugin.post_init(self) | ||||
| 		self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') | ||||
| 		logging.warning("This implementation of XEP-0004 is deprecated.") | ||||
| 	 | ||||
| 		log.warning("This implementation of XEP-0004 is deprecated.") | ||||
|  | ||||
| 	def handler_message_xform(self, xml): | ||||
| 		object = self.handle_form(xml) | ||||
| 		self.xmpp.event("message_form", object) | ||||
| 	 | ||||
|  | ||||
| 	def handler_presence_xform(self, xml): | ||||
| 		object = self.handle_form(xml) | ||||
| 		self.xmpp.event("presence_form", object) | ||||
| 	 | ||||
|  | ||||
| 	def handle_form(self, xml): | ||||
| 		xmlform = xml.find('{jabber:x:data}x') | ||||
| 		object = self.buildForm(xmlform) | ||||
| 		self.xmpp.event("message_xform", object) | ||||
| 		return object | ||||
| 	 | ||||
|  | ||||
| 	def buildForm(self, xml): | ||||
| 		form = Form(ftype=xml.attrib['type']) | ||||
| 		form.fromXML(xml) | ||||
| @@ -51,12 +55,12 @@ class FieldContainer(object): | ||||
| 		self.fields = [] | ||||
| 		self.field = {} | ||||
| 		self.stanza = stanza | ||||
| 	 | ||||
|  | ||||
| 	def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None): | ||||
| 		self.field[var] = FormField(var, ftype, label, desc, required, value) | ||||
| 		self.fields.append(self.field[var]) | ||||
| 		return self.field[var] | ||||
| 	 | ||||
|  | ||||
| 	def buildField(self, xml): | ||||
| 		self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single')) | ||||
| 		self.fields.append(self.field[xml.get('var', '__unnamed__')]) | ||||
| @@ -66,13 +70,13 @@ class FieldContainer(object): | ||||
| 		self.stanza = xml.tag | ||||
| 		for field in xml.findall('{jabber:x:data}field'): | ||||
| 			self.buildField(field) | ||||
| 	 | ||||
|  | ||||
| 	def getXML(self, ftype): | ||||
| 		container = ET.Element(self.stanza) | ||||
| 		for field in self.fields: | ||||
| 			container.append(field.getXML(ftype)) | ||||
| 		return container | ||||
| 	 | ||||
|  | ||||
| class Form(FieldContainer): | ||||
| 	types = ('form', 'submit', 'cancel', 'result') | ||||
| 	def __init__(self, xmpp=None, ftype='form', title='', instructions=''): | ||||
| @@ -85,7 +89,7 @@ class Form(FieldContainer): | ||||
| 		self.instructions = instructions | ||||
| 		self.reported = [] | ||||
| 		self.items = [] | ||||
| 	 | ||||
|  | ||||
| 	def merge(self, form2): | ||||
| 		form1 = Form(ftype=self.type) | ||||
| 		form1.fromXML(self.getXML(self.type)) | ||||
| @@ -98,18 +102,18 @@ class Form(FieldContainer): | ||||
| 				if (option, label) not in form1.field[field.var].options: | ||||
| 					form1.fields[field.var].addOption(option, label) | ||||
| 		return form1 | ||||
| 	 | ||||
|  | ||||
| 	def copy(self): | ||||
| 		newform = Form(ftype=self.type) | ||||
| 		newform.fromXML(self.getXML(self.type)) | ||||
| 		return newform | ||||
| 	 | ||||
|  | ||||
| 	def update(self, form): | ||||
| 		values = form.getValues() | ||||
| 		for var in values: | ||||
| 			if var in self.fields: | ||||
| 				self.fields[var].setValue(self.fields[var]) | ||||
| 	 | ||||
|  | ||||
| 	def getValues(self): | ||||
| 		result = {} | ||||
| 		for field in self.fields: | ||||
| @@ -118,7 +122,7 @@ class Form(FieldContainer): | ||||
| 				value = value[0] | ||||
| 			result[field.var] = value | ||||
| 		return result | ||||
| 	 | ||||
|  | ||||
| 	def setValues(self, values={}): | ||||
| 		for field in values: | ||||
| 			if field in self.field: | ||||
| @@ -127,10 +131,10 @@ class Form(FieldContainer): | ||||
| 						self.field[field].setValue(value) | ||||
| 				else: | ||||
| 					self.field[field].setValue(values[field]) | ||||
| 	 | ||||
|  | ||||
| 	def fromXML(self, xml): | ||||
| 		self.buildForm(xml) | ||||
| 	 | ||||
|  | ||||
| 	def addItem(self): | ||||
| 		newitem = FieldContainer('item') | ||||
| 		self.items.append(newitem) | ||||
| @@ -148,21 +152,21 @@ class Form(FieldContainer): | ||||
| 	def buildReported(self, xml): | ||||
| 		reported = self.addReported() | ||||
| 		reported.buildContainer(xml) | ||||
| 	 | ||||
|  | ||||
| 	def setTitle(self, title): | ||||
| 		self.title = title | ||||
| 	 | ||||
|  | ||||
| 	def setInstructions(self, instructions): | ||||
| 		self.instructions = instructions | ||||
| 	 | ||||
|  | ||||
| 	def setType(self, ftype): | ||||
| 		self.type = ftype | ||||
| 	 | ||||
|  | ||||
| 	def getXMLMessage(self, to): | ||||
| 		msg = self.xmpp.makeMessage(to) | ||||
| 		msg.append(self.getXML()) | ||||
| 		return msg | ||||
| 	 | ||||
|  | ||||
| 	def buildForm(self, xml): | ||||
| 		self.type = xml.get('type', 'form') | ||||
| 		if xml.find('{jabber:x:data}title') is not None: | ||||
| @@ -175,7 +179,7 @@ class Form(FieldContainer): | ||||
| 			self.buildReported(reported) | ||||
| 		for item in xml.findall('{jabber:x:data}item'): | ||||
| 			self.buildItem(item) | ||||
| 	 | ||||
|  | ||||
| 	#def getXML(self, tostring = False): | ||||
| 	def getXML(self, ftype=None): | ||||
| 		if ftype: | ||||
| @@ -199,7 +203,7 @@ class Form(FieldContainer): | ||||
| 		#if tostring: | ||||
| 		#	form = self.xmpp.tostring(form) | ||||
| 		return form | ||||
| 	 | ||||
|  | ||||
| 	def getXHTML(self): | ||||
| 		form = ET.Element('{http://www.w3.org/1999/xhtml}form') | ||||
| 		if self.title: | ||||
| @@ -217,8 +221,8 @@ class Form(FieldContainer): | ||||
| 		for field in self.items: | ||||
| 			form.append(field.getXHTML()) | ||||
| 		return form | ||||
| 		 | ||||
| 	 | ||||
|  | ||||
|  | ||||
| 	def makeSubmit(self): | ||||
| 		self.setType('submit') | ||||
|  | ||||
| @@ -246,13 +250,13 @@ class FormField(object): | ||||
| 			self.islinebreak = False | ||||
| 		if value: | ||||
| 			self.setValue(value) | ||||
| 	 | ||||
|  | ||||
| 	def addOption(self, value, label): | ||||
| 		if self.islist: | ||||
| 			self.options.append((value, label)) | ||||
| 		else: | ||||
| 			raise ValueError("Cannot add options to non-list type field.") | ||||
| 	 | ||||
|  | ||||
| 	def setTrue(self): | ||||
| 		if self.type == 'boolean': | ||||
| 			self.value = [True] | ||||
| @@ -263,10 +267,10 @@ class FormField(object): | ||||
|  | ||||
| 	def require(self): | ||||
| 		self.required = True | ||||
| 	 | ||||
|  | ||||
| 	def setDescription(self, desc): | ||||
| 		self.desc = desc | ||||
| 	 | ||||
|  | ||||
| 	def setValue(self, value): | ||||
| 		if self.type == 'boolean': | ||||
| 			if value in ('1', 1, True, 'true', 'True', 'yes'): | ||||
| @@ -291,10 +295,10 @@ class FormField(object): | ||||
| 				pass | ||||
| 		else: | ||||
| 			self.value = '' | ||||
| 	 | ||||
|  | ||||
| 	def setAnswer(self, value): | ||||
| 		self.setValue(value) | ||||
| 	 | ||||
|  | ||||
| 	def buildField(self, xml): | ||||
| 		self.type = xml.get('type', 'text-single') | ||||
| 		self.label = xml.get('label', '') | ||||
| @@ -306,7 +310,7 @@ class FormField(object): | ||||
| 			self.require() | ||||
| 		if xml.find('{jabber:x:data}desc') is not None: | ||||
| 			self.setDescription(xml.find('{jabber:x:data}desc').text) | ||||
| 	 | ||||
|  | ||||
| 	def getXML(self, ftype): | ||||
| 		field = ET.Element('{jabber:x:data}field') | ||||
| 		if ftype != 'result': | ||||
| @@ -342,7 +346,7 @@ class FormField(object): | ||||
| 				valuexml.text = value | ||||
| 			field.append(valuexml) | ||||
| 		return field | ||||
| 	 | ||||
|  | ||||
| 	def getXHTML(self): | ||||
| 		field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type}) | ||||
| 		if self.label: | ||||
| @@ -414,4 +418,4 @@ class FormField(object): | ||||
| 				pass | ||||
| 		label.append(formf) | ||||
| 		return field | ||||
| 		 | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import logging | ||||
| from xml.etree import cElementTree as ET | ||||
| import time | ||||
| 
 | ||||
| class xep_0050(base.base_plugin): | ||||
| class old_0050(base.base_plugin): | ||||
| 	""" | ||||
| 	XEP-0050 Ad-Hoc Commands | ||||
| 	""" | ||||
| @@ -110,7 +110,7 @@ class xep_0050(base.base_plugin): | ||||
| 		if not id: | ||||
| 			id = self.xmpp.getNewId() | ||||
| 		iq = self.xmpp.makeIqResult(id) | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		iq.attrib['to'] = to | ||||
| 		command = ET.Element('{http://jabber.org/protocol/commands}command') | ||||
| 		command.attrib['node'] = node | ||||
| @@ -237,6 +237,8 @@ class Unsubscribe(ElementBase): | ||||
| 	def getJid(self): | ||||
| 		return JID(self._getAttr('jid')) | ||||
|  | ||||
| registerStanzaPlugin(Pubsub, Unsubscribe) | ||||
|  | ||||
| class Subscribe(ElementBase): | ||||
| 	namespace = 'http://jabber.org/protocol/pubsub' | ||||
| 	name = 'subscribe' | ||||
|   | ||||
| @@ -13,7 +13,9 @@ 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 | ||||
| import types | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class Form(ElementBase): | ||||
| @@ -33,7 +35,7 @@ class Form(ElementBase): | ||||
| 		if title is not None: | ||||
| 			self['title'] = title | ||||
| 		self.field = FieldAccessor(self) | ||||
| 	 | ||||
|  | ||||
| 	def setup(self, xml=None): | ||||
| 		if ElementBase.setup(self, xml): #if we had to generate xml | ||||
| 			self['type'] = 'form' | ||||
| @@ -55,11 +57,12 @@ class Form(ElementBase): | ||||
| 		return field | ||||
|  | ||||
| 	def getXML(self, type='submit'): | ||||
| 		logging.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py") | ||||
| 		self['type'] = type | ||||
| 		log.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py") | ||||
| 		return self.xml | ||||
| 	 | ||||
|  | ||||
| 	def fromXML(self, xml): | ||||
| 		logging.warning("Form.fromXML() is deprecated API compatibility with plugins/old_0004.py") | ||||
| 		log.warning("Form.fromXML() is deprecated API compatibility with plugins/old_0004.py") | ||||
| 		n = Form(xml=xml) | ||||
| 		return n | ||||
|  | ||||
| @@ -113,10 +116,10 @@ class Form(ElementBase): | ||||
| 		reportedXML = self.xml.find('{%s}reported' % self.namespace) | ||||
| 		if reportedXML is not None: | ||||
| 			self.xml.remove(reportedXML) | ||||
| 	 | ||||
|  | ||||
| 	def getFields(self, use_dict=False): | ||||
| 		fields = {} if use_dict else [] | ||||
| 		fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)     | ||||
| 		fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) | ||||
| 		for fieldXML in fieldsXML: | ||||
| 			field = FormField(xml=fieldXML) | ||||
| 			if use_dict: | ||||
| @@ -144,7 +147,7 @@ class Form(ElementBase): | ||||
|  | ||||
| 	def getReported(self): | ||||
| 		fields = {} | ||||
| 		fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,  | ||||
| 		fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, | ||||
| 									 FormField.namespace)) | ||||
| 		for fieldXML in fieldsXML: | ||||
| 			field = FormField(xml=fieldXML) | ||||
| @@ -197,10 +200,10 @@ class Form(ElementBase): | ||||
| 		fields = self.getFields(use_dict=True) | ||||
| 		for field in values: | ||||
| 			fields[field]['value'] = values[field] | ||||
| 	 | ||||
|  | ||||
| 	def merge(self, other): | ||||
| 		new = copy.copy(self) | ||||
| 		if type(other) == types.DictType: | ||||
| 		if type(other) == dict: | ||||
| 			new.setValues(other) | ||||
| 			return new | ||||
| 		nfields = new.getFields(use_dict=True) | ||||
| @@ -212,7 +215,7 @@ class Form(ElementBase): | ||||
| class FieldAccessor(object): | ||||
| 	def __init__(self, form): | ||||
| 		self.form = form | ||||
| 	 | ||||
|  | ||||
| 	def __getitem__(self, key): | ||||
| 		return self.form.getFields(use_dict=True)[key] | ||||
|  | ||||
| @@ -366,21 +369,21 @@ class xep_0004(base.base_plugin): | ||||
|  | ||||
| 		self.xmpp.registerHandler( | ||||
| 			Callback('Data Form', | ||||
| 				 MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns,  | ||||
| 				 MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns, | ||||
| 								   Form.namespace)), | ||||
| 				 self.handle_form)) | ||||
|  | ||||
| 		registerStanzaPlugin(FormField, FieldOption) | ||||
| 		registerStanzaPlugin(Form, FormField) | ||||
| 		registerStanzaPlugin(Message, Form) | ||||
| 	 | ||||
|  | ||||
| 	def makeForm(self, ftype='form', title='', instructions=''): | ||||
| 		f = Form() | ||||
| 		f['type'] = ftype | ||||
| 		f['title'] = title | ||||
| 		f['instructions'] = instructions | ||||
| 		return f | ||||
| 	 | ||||
|  | ||||
| 	def post_init(self): | ||||
| 		base.base_plugin.post_init(self) | ||||
| 		self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') | ||||
|   | ||||
							
								
								
									
										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 | ||||
							
								
								
									
										118
									
								
								sleekxmpp/plugins/xep_0012.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								sleekxmpp/plugins/xep_0012.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| """ | ||||
|     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 datetime import datetime | ||||
| import logging | ||||
|  | ||||
| from . import base | ||||
| from .. stanza.iq import Iq | ||||
| from .. xmlstream.handler.callback import Callback | ||||
| from .. xmlstream.matcher.xpath import MatchXPath | ||||
| from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class LastActivity(ElementBase): | ||||
|     name = 'query' | ||||
|     namespace = 'jabber:iq:last' | ||||
|     plugin_attrib = 'last_activity' | ||||
|     interfaces = set(('seconds', 'status')) | ||||
|  | ||||
|     def get_seconds(self): | ||||
|         return int(self._get_attr('seconds')) | ||||
|  | ||||
|     def set_seconds(self, value): | ||||
|         self._set_attr('seconds', str(value)) | ||||
|  | ||||
|     def get_status(self): | ||||
|         return self.xml.text | ||||
|  | ||||
|     def set_status(self, value): | ||||
|         self.xml.text = str(value) | ||||
|  | ||||
|     def del_status(self): | ||||
|         self.xml.text = '' | ||||
|  | ||||
| class xep_0012(base.base_plugin): | ||||
|     """ | ||||
|     XEP-0012 Last Activity | ||||
|     """ | ||||
|     def plugin_init(self): | ||||
|         self.description = "Last Activity" | ||||
|         self.xep = "0012" | ||||
|  | ||||
|         self.xmpp.registerHandler( | ||||
|             Callback('Last Activity', | ||||
|                  MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns, | ||||
|                                   LastActivity.namespace)), | ||||
|                  self.handle_last_activity_query)) | ||||
|         register_stanza_plugin(Iq, LastActivity) | ||||
|  | ||||
|         self.xmpp.add_event_handler('last_activity_request', self.handle_last_activity) | ||||
|  | ||||
|  | ||||
|     def post_init(self): | ||||
|         base.base_plugin.post_init(self) | ||||
|         if self.xmpp.is_component: | ||||
|             # We are a component, so we track the uptime | ||||
|             self.xmpp.add_event_handler("session_start", self._reset_uptime) | ||||
|             self._start_datetime = datetime.now() | ||||
|         self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:last') | ||||
|  | ||||
|     def _reset_uptime(self, event): | ||||
|         self._start_datetime = datetime.now() | ||||
|  | ||||
|     def handle_last_activity_query(self, iq): | ||||
|         if iq['type'] == 'get': | ||||
|             log.debug("Last activity requested by %s" % iq['from']) | ||||
|             self.xmpp.event('last_activity_request', iq) | ||||
|         elif iq['type'] == 'result': | ||||
|             log.debug("Last activity result from %s" % iq['from']) | ||||
|             self.xmpp.event('last_activity', iq) | ||||
|  | ||||
|     def handle_last_activity(self, iq): | ||||
|         jid = iq['from'] | ||||
|  | ||||
|         if self.xmpp.is_component: | ||||
|             # Send the uptime | ||||
|             result = LastActivity() | ||||
|             td = (datetime.now() - self._start_datetime) | ||||
|             result['seconds'] = td.seconds + td.days * 24 * 3600 | ||||
|             reply = iq.reply().setPayload(result.xml).send() | ||||
|         else: | ||||
|             barejid = JID(jid).bare | ||||
|             if barejid in self.xmpp.roster and ( self.xmpp.roster[barejid]['subscription'] in ('from', 'both') or | ||||
|                                                  barejid == self.xmpp.boundjid.bare ): | ||||
|                 # We don't know how to calculate it | ||||
|                 iq.reply().error().setPayload(iq['last_activity'].xml) | ||||
|                 iq['error']['code'] = '503' | ||||
|                 iq['error']['type'] = 'cancel' | ||||
|                 iq['error']['condition'] = 'service-unavailable' | ||||
|                 iq.send() | ||||
|             else: | ||||
|                 iq.reply().error().setPayload(iq['last_activity'].xml) | ||||
|                 iq['error']['code'] = '403' | ||||
|                 iq['error']['type'] = 'auth' | ||||
|                 iq['error']['condition'] = 'forbidden' | ||||
|                 iq.send() | ||||
|  | ||||
|     def get_last_activity(self, jid): | ||||
|         """Query the LastActivity of jid and return it in seconds""" | ||||
|         iq = self.xmpp.makeIqGet() | ||||
|         query = LastActivity() | ||||
|         iq.append(query.xml) | ||||
|         iq.attrib['to'] = jid | ||||
|         iq.attrib['from'] = self.xmpp.boundjid.full | ||||
|         id = iq.get('id') | ||||
|         result = iq.send() | ||||
|         if result and result is not None and result.get('type', 'error') != 'error': | ||||
|             return result['last_activity']['seconds'] | ||||
|         else: | ||||
|             return False | ||||
| @@ -1,327 +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 | ||||
|  | ||||
| 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': | ||||
| 			logging.debug("Items requested by %s" % iq['from']) | ||||
| 			self.xmpp.event('disco_items_request', iq) | ||||
| 		elif iq['type'] == 'result': | ||||
| 			logging.debug("Items result from %s" % iq['from']) | ||||
| 			self.xmpp.event('disco_items', iq) | ||||
|  | ||||
| 	def handle_info_query(self, iq): | ||||
| 		if iq['type'] == 'get': | ||||
| 			logging.debug("Info requested by %s" % iq['from']) | ||||
| 			self.xmpp.event('disco_info_request', iq) | ||||
| 		elif iq['type'] == 'result': | ||||
| 			logging.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. | ||||
| 		""" | ||||
| 		handlers = self.xmpp.event_handlers['disco_info_request'] | ||||
| 		if not forwarded and len(handlers) > 1: | ||||
| 			return | ||||
|  | ||||
| 		node_name = iq['disco_info']['node'] | ||||
| 		if not node_name: | ||||
| 			node_name = 'main' | ||||
|  | ||||
| 		logging.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: | ||||
| 			logging.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. | ||||
| 		""" | ||||
| 		handlers = self.xmpp.event_handlers['disco_items_request'] | ||||
| 		if not forwarded and len(handlers) > 1: | ||||
| 			return | ||||
|  | ||||
| 		node_name = iq['disco_items']['node'] | ||||
| 		if not node_name: | ||||
| 			node_name = 'main' | ||||
|  | ||||
| 		logging.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:	 | ||||
| 			logging.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)) | ||||
| @@ -2,7 +2,7 @@ | ||||
|     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 __future__ import with_statement | ||||
| @@ -15,319 +15,339 @@ from .. xmlstream.handler.callback import Callback | ||||
| from .. xmlstream.matcher.xpath import MatchXPath | ||||
| from .. xmlstream.matcher.xmlmask import MatchXMLMask | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class MUCPresence(ElementBase): | ||||
| 	name = 'x' | ||||
| 	namespace = 'http://jabber.org/protocol/muc#user' | ||||
| 	plugin_attrib = 'muc' | ||||
| 	interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room')) | ||||
| 	affiliations = set(('', )) | ||||
| 	roles = set(('', )) | ||||
|     name = 'x' | ||||
|     namespace = 'http://jabber.org/protocol/muc#user' | ||||
|     plugin_attrib = 'muc' | ||||
|     interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room')) | ||||
|     affiliations = set(('', )) | ||||
|     roles = set(('', )) | ||||
|  | ||||
| 	def getXMLItem(self): | ||||
| 		item = self.xml.find('{http://jabber.org/protocol/muc#user}item') | ||||
| 		if item is None: | ||||
| 			item = ET.Element('{http://jabber.org/protocol/muc#user}item') | ||||
| 			self.xml.append(item) | ||||
| 		return item | ||||
|     def getXMLItem(self): | ||||
|         item = self.xml.find('{http://jabber.org/protocol/muc#user}item') | ||||
|         if item is None: | ||||
|             item = ET.Element('{http://jabber.org/protocol/muc#user}item') | ||||
|             self.xml.append(item) | ||||
|         return item | ||||
|  | ||||
| 	def getAffiliation(self): | ||||
| 		#TODO if no affilation, set it to the default and return default | ||||
| 		item = self.getXMLItem() | ||||
| 		return item.get('affiliation', '') | ||||
| 	 | ||||
| 	def setAffiliation(self, value): | ||||
| 		item = self.getXMLItem() | ||||
| 		#TODO check for valid affiliation | ||||
| 		item.attrib['affiliation'] = value | ||||
| 		return self | ||||
| 	 | ||||
| 	def delAffiliation(self): | ||||
| 		item = self.getXMLItem() | ||||
| 		#TODO set default affiliation | ||||
| 		if 'affiliation' in item.attrib: del item.attrib['affiliation'] | ||||
| 		return self | ||||
| 	 | ||||
| 	def getJid(self): | ||||
| 		item = self.getXMLItem() | ||||
| 		return JID(item.get('jid', '')) | ||||
| 	 | ||||
| 	def setJid(self, value): | ||||
| 		item = self.getXMLItem() | ||||
| 		if not isinstance(value, str): | ||||
| 			value = str(value) | ||||
| 		item.attrib['jid'] = value | ||||
| 		return self | ||||
| 	 | ||||
| 	def delJid(self): | ||||
| 		item = self.getXMLItem() | ||||
| 		if 'jid' in item.attrib: del item.attrib['jid'] | ||||
| 		return self | ||||
| 		 | ||||
| 	def getRole(self): | ||||
| 		item = self.getXMLItem() | ||||
| 		#TODO get default role, set default role if none | ||||
| 		return item.get('role', '') | ||||
| 	 | ||||
| 	def setRole(self, value): | ||||
| 		item = self.getXMLItem() | ||||
| 		#TODO check for valid role | ||||
| 		item.attrib['role'] = value | ||||
| 		return self | ||||
| 	 | ||||
| 	def delRole(self): | ||||
| 		item = self.getXMLItem() | ||||
| 		#TODO set default role | ||||
| 		if 'role' in item.attrib: del item.attrib['role'] | ||||
| 		return self | ||||
| 	 | ||||
| 	def getNick(self): | ||||
| 		return self.parent()['from'].resource | ||||
| 	 | ||||
| 	def getRoom(self): | ||||
| 		return self.parent()['from'].bare | ||||
| 	 | ||||
| 	def setNick(self, value): | ||||
| 		logging.warning("Cannot set nick through mucpresence plugin.") | ||||
| 		return self | ||||
| 	 | ||||
| 	def setRoom(self, value): | ||||
| 		logging.warning("Cannot set room through mucpresence plugin.") | ||||
| 		return self | ||||
| 	 | ||||
| 	def delNick(self): | ||||
| 		logging.warning("Cannot delete nick through mucpresence plugin.") | ||||
| 		return self | ||||
| 	 | ||||
| 	def delRoom(self): | ||||
| 		logging.warning("Cannot delete room through mucpresence plugin.") | ||||
| 		return self | ||||
|     def getAffiliation(self): | ||||
|         #TODO if no affilation, set it to the default and return default | ||||
|         item = self.getXMLItem() | ||||
|         return item.get('affiliation', '') | ||||
|  | ||||
|     def setAffiliation(self, value): | ||||
|         item = self.getXMLItem() | ||||
|         #TODO check for valid affiliation | ||||
|         item.attrib['affiliation'] = value | ||||
|         return self | ||||
|  | ||||
|     def delAffiliation(self): | ||||
|         item = self.getXMLItem() | ||||
|         #TODO set default affiliation | ||||
|         if 'affiliation' in item.attrib: del item.attrib['affiliation'] | ||||
|         return self | ||||
|  | ||||
|     def getJid(self): | ||||
|         item = self.getXMLItem() | ||||
|         return JID(item.get('jid', '')) | ||||
|  | ||||
|     def setJid(self, value): | ||||
|         item = self.getXMLItem() | ||||
|         if not isinstance(value, str): | ||||
|             value = str(value) | ||||
|         item.attrib['jid'] = value | ||||
|         return self | ||||
|  | ||||
|     def delJid(self): | ||||
|         item = self.getXMLItem() | ||||
|         if 'jid' in item.attrib: del item.attrib['jid'] | ||||
|         return self | ||||
|  | ||||
|     def getRole(self): | ||||
|         item = self.getXMLItem() | ||||
|         #TODO get default role, set default role if none | ||||
|         return item.get('role', '') | ||||
|  | ||||
|     def setRole(self, value): | ||||
|         item = self.getXMLItem() | ||||
|         #TODO check for valid role | ||||
|         item.attrib['role'] = value | ||||
|         return self | ||||
|  | ||||
|     def delRole(self): | ||||
|         item = self.getXMLItem() | ||||
|         #TODO set default role | ||||
|         if 'role' in item.attrib: del item.attrib['role'] | ||||
|         return self | ||||
|  | ||||
|     def getNick(self): | ||||
|         return self.parent()['from'].resource | ||||
|  | ||||
|     def getRoom(self): | ||||
|         return self.parent()['from'].bare | ||||
|  | ||||
|     def setNick(self, value): | ||||
|         log.warning("Cannot set nick through mucpresence plugin.") | ||||
|         return self | ||||
|  | ||||
|     def setRoom(self, value): | ||||
|         log.warning("Cannot set room through mucpresence plugin.") | ||||
|         return self | ||||
|  | ||||
|     def delNick(self): | ||||
|         log.warning("Cannot delete nick through mucpresence plugin.") | ||||
|         return self | ||||
|  | ||||
|     def delRoom(self): | ||||
|         log.warning("Cannot delete room through mucpresence plugin.") | ||||
|         return self | ||||
|  | ||||
| class xep_0045(base.base_plugin): | ||||
| 	""" | ||||
| 	Impliments XEP-0045 Multi User Chat | ||||
| 	""" | ||||
| 	 | ||||
| 	def plugin_init(self): | ||||
| 		self.rooms = {} | ||||
| 		self.ourNicks = {} | ||||
| 		self.xep = '0045' | ||||
| 		self.description = 'Multi User Chat' | ||||
| 		# load MUC support in presence stanzas | ||||
| 		registerStanzaPlugin(Presence, MUCPresence) | ||||
| 		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)) | ||||
| 	 | ||||
| 	def handle_groupchat_presence(self, pr): | ||||
| 		""" Handle a presence in a muc. | ||||
| 		""" | ||||
| 		got_offline = False | ||||
| 		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 | ||||
| 		logging.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): | ||||
| 		""" Handle a message event in a muc. | ||||
| 		""" | ||||
| 		self.xmpp.event('groupchat_message', msg) | ||||
| 		self.xmpp.event("muc::%s::message" % msg['from'].bare, msg) | ||||
| 		        | ||||
| 	def jidInRoom(self, room, jid): | ||||
| 		for nick in self.rooms[room]: | ||||
| 			entry = self.rooms[room][nick] | ||||
| 			if entry is not None and entry['jid'].full == jid: | ||||
| 				return True | ||||
| 		return False | ||||
| 	 | ||||
| 	def getNick(self, room, jid): | ||||
| 		for nick in self.rooms[room]: | ||||
| 			entry = self.rooms[room][nick] | ||||
| 			if entry is not None and entry['jid'].full == jid: | ||||
| 				return nick | ||||
|     """ | ||||
|     Implements XEP-0045 Multi User Chat | ||||
|     """ | ||||
|  | ||||
| 	def getRoomForm(self, room, ifrom=None): | ||||
| 		iq = self.xmpp.makeIqGet() | ||||
| 		iq['to'] = room | ||||
| 		if ifrom is not None: | ||||
| 			iq['from'] = ifrom | ||||
| 		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): | ||||
| 		if form is None: | ||||
| 			form = self.getRoomForm(room, ifrom=ifrom) | ||||
| 			#form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit') | ||||
| 			#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')	 | ||||
| 		iq = self.xmpp.makeIqSet() | ||||
| 		iq['to'] = room | ||||
| 		if ifrom is not None: | ||||
| 			iq['from'] = ifrom | ||||
| 		query = ET.Element('{http://jabber.org/protocol/muc#owner}query') | ||||
| 		form = form.getXML('submit') | ||||
| 		query.append(form) | ||||
| 		iq.append(query) | ||||
| 		result = iq.send() | ||||
| 		if result['type'] == 'error': | ||||
| 			return False | ||||
| 		return True | ||||
| 	 | ||||
| 	def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None): | ||||
| 		""" Join the specified room, requesting 'maxhistory' lines of history. | ||||
| 		""" | ||||
| 		stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow) | ||||
| 		x = ET.Element('{http://jabber.org/protocol/muc}x') | ||||
| 		if password: | ||||
| 			passelement = ET.Element('password') | ||||
| 			passelement.text = password | ||||
| 			x.append(passelement) | ||||
| 		if maxhistory: | ||||
| 			history = ET.Element('history') | ||||
| 			if maxhistory ==  "0": | ||||
| 				history.attrib['maxchars'] = maxhistory | ||||
| 			else: | ||||
| 				history.attrib['maxstanzas'] = maxhistory | ||||
| 			x.append(history) | ||||
| 		stanza.append(x) | ||||
| 		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): | ||||
| 		iq = self.xmpp.makeIqSet() | ||||
| 		if ifrom is not None: | ||||
| 			iq['from'] = ifrom | ||||
| 		iq['to'] = room | ||||
| 		query = ET.Element('{http://jabber.org/protocol/muc#owner}query') | ||||
| 		destroy = ET.Element('destroy') | ||||
| 		if altroom: | ||||
| 			destroy.attrib['jid'] = altroom | ||||
| 		xreason = ET.Element('reason') | ||||
| 		xreason.text = reason | ||||
| 		destroy.append(xreason) | ||||
| 		query.append(destroy) | ||||
| 		iq.append(query) | ||||
| 		r = iq.send() | ||||
| 		if r is False or r['type'] == 'error': | ||||
| 			return False | ||||
| 		return True | ||||
|     def plugin_init(self): | ||||
|         self.rooms = {} | ||||
|         self.ourNicks = {} | ||||
|         self.xep = '0045' | ||||
|         self.description = 'Multi User Chat' | ||||
|         # load MUC support in presence stanzas | ||||
|         registerStanzaPlugin(Presence, MUCPresence) | ||||
|         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('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 setAffiliation(self, room, jid=None, nick=None, affiliation='member'): | ||||
| 		""" Change room affiliation.""" | ||||
| 		if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): | ||||
| 			raise TypeError | ||||
| 		query = ET.Element('{http://jabber.org/protocol/muc#admin}query') | ||||
| 		if nick is not None: | ||||
| 			item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})	 | ||||
| 		else: | ||||
| 			item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})	 | ||||
| 		query.append(item) | ||||
| 		iq = self.xmpp.makeIqSet(query) | ||||
| 		iq['to'] = room | ||||
| 		result = iq.send() | ||||
| 		if result is False or result['type'] != 'result': | ||||
| 			raise ValueError | ||||
| 		return True | ||||
| 	 | ||||
| 	def invite(self, room, jid, reason=''): | ||||
| 		""" Invite a jid to a room.""" | ||||
| 		msg = self.xmpp.makeMessage(room) | ||||
| 		msg['from'] = self.xmpp.jid | ||||
| 		x = ET.Element('{http://jabber.org/protocol/muc#user}x') | ||||
| 		invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid}) | ||||
| 		if reason: | ||||
| 			rxml = ET.Element('reason') | ||||
| 			rxml.text = reason | ||||
| 			invite.append(rxml) | ||||
| 		x.append(invite) | ||||
| 		msg.append(x) | ||||
| 		self.xmpp.send(msg) | ||||
|     def handle_groupchat_invite(self, inv): | ||||
|         """ Handle an invite into a muc. | ||||
|         """ | ||||
|         logging.debug("MUC invite to %s from %s: %s" % (inv['from'], inv["from"], inv)) | ||||
|         if inv['from'] not in self.rooms.keys(): | ||||
|             self.xmpp.event("groupchat_invite", inv) | ||||
|  | ||||
| 	def leaveMUC(self, room, nick, msg=''): | ||||
| 		""" Leave the specified room. | ||||
| 		""" | ||||
| 		if msg: | ||||
| 			self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg) | ||||
| 		else: | ||||
| 			self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick)) | ||||
| 		del self.rooms[room] | ||||
| 	 | ||||
| 	def getRoomConfig(self, room): | ||||
| 		iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner') | ||||
| 		iq['to'] = room | ||||
| 		iq['from'] = self.xmpp.jid | ||||
| 		result = 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 cancelConfig(self, room): | ||||
| 		query = ET.Element('{http://jabber.org/protocol/muc#owner}query') | ||||
| 		x = ET.Element('{jabber:x:data}x', type='cancel') | ||||
| 		query.append(x) | ||||
| 		iq = self.xmpp.makeIqSet(query) | ||||
| 		iq.send() | ||||
| 	 | ||||
| 	def setRoomConfig(self, room, config): | ||||
| 		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'] = self.xmpp.jid | ||||
| 		iq.send() | ||||
| 		 | ||||
| 	def getJoinedRooms(self): | ||||
| 		return self.rooms.keys() | ||||
| 		 | ||||
| 	def getOurJidInRoom(self, roomJid): | ||||
| 		""" Return the jid we're using in a room. | ||||
| 		""" | ||||
| 		return "%s/%s" % (roomJid, self.ourNicks[roomJid]) | ||||
| 		 | ||||
| 	def getJidProperty(self, room, nick, jidProperty): | ||||
| 		""" Get the property of a nick in a room, such as its 'jid' or 'affiliation' | ||||
| 			If not found, return None. | ||||
| 		""" | ||||
| 		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): | ||||
| 		""" Get the list of nicks in a room. | ||||
| 		""" | ||||
| 		if room not in self.rooms.keys(): | ||||
| 			return None | ||||
| 		return self.rooms[room].keys() | ||||
|     def handle_groupchat_presence(self, pr): | ||||
|         """ Handle a presence in a muc. | ||||
|         """ | ||||
|         got_offline = False | ||||
|         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_message(self, msg): | ||||
|         """ Handle a message event in a muc. | ||||
|         """ | ||||
|         self.xmpp.event('groupchat_message', msg) | ||||
|         self.xmpp.event("muc::%s::message" % msg['from'].bare, msg) | ||||
|  | ||||
|     def handle_groupchat_subject(self, msg): | ||||
|         """ Handle a message coming from a muc indicating | ||||
|         a change of subject (or announcing it when joining the room) | ||||
|         """ | ||||
|         self.xmpp.event('groupchat_subject', msg) | ||||
|  | ||||
|     def jidInRoom(self, room, jid): | ||||
|         for nick in self.rooms[room]: | ||||
|             entry = self.rooms[room][nick] | ||||
|             if entry is not None and entry['jid'].full == jid: | ||||
|                 return True | ||||
|         return False | ||||
|  | ||||
|     def getNick(self, room, jid): | ||||
|         for nick in self.rooms[room]: | ||||
|             entry = self.rooms[room][nick] | ||||
|             if entry is not None and entry['jid'].full == jid: | ||||
|                 return nick | ||||
|  | ||||
|     def getRoomForm(self, room, ifrom=None): | ||||
|         iq = self.xmpp.makeIqGet() | ||||
|         iq['to'] = room | ||||
|         if ifrom is not None: | ||||
|             iq['from'] = ifrom | ||||
|         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): | ||||
|         if form is None: | ||||
|             form = self.getRoomForm(room, ifrom=ifrom) | ||||
|             #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit') | ||||
|             #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig') | ||||
|         iq = self.xmpp.makeIqSet() | ||||
|         iq['to'] = room | ||||
|         if ifrom is not None: | ||||
|             iq['from'] = ifrom | ||||
|         query = ET.Element('{http://jabber.org/protocol/muc#owner}query') | ||||
|         form = form.getXML('submit') | ||||
|         query.append(form) | ||||
|         iq.append(query) | ||||
|         result = iq.send() | ||||
|         if result['type'] == 'error': | ||||
|             return False | ||||
|         return True | ||||
|  | ||||
|     def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None): | ||||
|         """ Join the specified room, requesting 'maxhistory' lines of history. | ||||
|         """ | ||||
|         stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow) | ||||
|         x = ET.Element('{http://jabber.org/protocol/muc}x') | ||||
|         if password: | ||||
|             passelement = ET.Element('password') | ||||
|             passelement.text = password | ||||
|             x.append(passelement) | ||||
|         if maxhistory: | ||||
|             history = ET.Element('history') | ||||
|             if maxhistory ==  "0": | ||||
|                 history.attrib['maxchars'] = maxhistory | ||||
|             else: | ||||
|                 history.attrib['maxstanzas'] = maxhistory | ||||
|             x.append(history) | ||||
|         stanza.append(x) | ||||
|         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): | ||||
|         iq = self.xmpp.makeIqSet() | ||||
|         if ifrom is not None: | ||||
|             iq['from'] = ifrom | ||||
|         iq['to'] = room | ||||
|         query = ET.Element('{http://jabber.org/protocol/muc#owner}query') | ||||
|         destroy = ET.Element('destroy') | ||||
|         if altroom: | ||||
|             destroy.attrib['jid'] = altroom | ||||
|         xreason = ET.Element('reason') | ||||
|         xreason.text = reason | ||||
|         destroy.append(xreason) | ||||
|         query.append(destroy) | ||||
|         iq.append(query) | ||||
|         r = iq.send() | ||||
|         if r is False or r['type'] == 'error': | ||||
|             return False | ||||
|         return True | ||||
|  | ||||
|     def setAffiliation(self, room, jid=None, nick=None, affiliation='member'): | ||||
|         """ Change room affiliation.""" | ||||
|         if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): | ||||
|             raise TypeError | ||||
|         query = ET.Element('{http://jabber.org/protocol/muc#admin}query') | ||||
|         if nick is not None: | ||||
|             item = ET.Element('item', {'affiliation':affiliation, 'nick':nick}) | ||||
|         else: | ||||
|             item = ET.Element('item', {'affiliation':affiliation, 'jid':jid}) | ||||
|         query.append(item) | ||||
|         iq = self.xmpp.makeIqSet(query) | ||||
|         iq['to'] = room | ||||
|         result = iq.send() | ||||
|         if result is False or result['type'] != 'result': | ||||
|             raise ValueError | ||||
|         return True | ||||
|  | ||||
|     def invite(self, room, jid, reason='', mfrom=''): | ||||
|         """ Invite a jid to a room.""" | ||||
|         msg = self.xmpp.makeMessage(room) | ||||
|         msg['from'] = mfrom | ||||
|         x = ET.Element('{http://jabber.org/protocol/muc#user}x') | ||||
|         invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid}) | ||||
|         if reason: | ||||
|             rxml = ET.Element('reason') | ||||
|             rxml.text = reason | ||||
|             invite.append(rxml) | ||||
|         x.append(invite) | ||||
|         msg.append(x) | ||||
|         self.xmpp.send(msg) | ||||
|  | ||||
|     def leaveMUC(self, room, nick, msg=''): | ||||
|         """ Leave the specified room. | ||||
|         """ | ||||
|         if msg: | ||||
|             self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg) | ||||
|         else: | ||||
|             self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick)) | ||||
|         del self.rooms[room] | ||||
|  | ||||
|     def getRoomConfig(self, room, ifrom=''): | ||||
|         iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner') | ||||
|         iq['to'] = room | ||||
|         iq['from'] = ifrom | ||||
|         result = 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 cancelConfig(self, room): | ||||
|         query = ET.Element('{http://jabber.org/protocol/muc#owner}query') | ||||
|         x = ET.Element('{jabber:x:data}x', type='cancel') | ||||
|         query.append(x) | ||||
|         iq = self.xmpp.makeIqSet(query) | ||||
|         iq['to'] = room | ||||
|         iq.send() | ||||
|  | ||||
|     def setRoomConfig(self, room, config, ifrom=''): | ||||
|         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 getJoinedRooms(self): | ||||
|         return self.rooms.keys() | ||||
|  | ||||
|     def getOurJidInRoom(self, roomJid): | ||||
|         """ Return the jid we're using in a room. | ||||
|         """ | ||||
|         return "%s/%s" % (roomJid, self.ourNicks[roomJid]) | ||||
|  | ||||
|     def getJidProperty(self, room, nick, jidProperty): | ||||
|         """ Get the property of a nick in a room, such as its 'jid' or 'affiliation' | ||||
|             If not found, return None. | ||||
|         """ | ||||
|         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): | ||||
|         """ 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 | ||||
| @@ -6,6 +6,10 @@ from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET | ||||
| from . import stanza_pubsub | ||||
| from . xep_0004 import Form | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class xep_0060(base.base_plugin): | ||||
| 	""" | ||||
| 	XEP-0060 Publish Subscribe | ||||
| @@ -14,7 +18,7 @@ class xep_0060(base.base_plugin): | ||||
| 	def plugin_init(self): | ||||
| 		self.xep = '0060' | ||||
| 		self.description = 'Publish-Subscribe' | ||||
| 	 | ||||
|  | ||||
| 	def create_node(self, jid, node, config=None, collection=False, ntype=None): | ||||
| 		pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') | ||||
| 		create = ET.Element('create') | ||||
| @@ -47,52 +51,52 @@ class xep_0060(base.base_plugin): | ||||
| 		pubsub.append(configure) | ||||
| 		iq = self.xmpp.makeIqSet(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		result = iq.send() | ||||
| 		if result is False or result is None or result['type'] == 'error': return False | ||||
| 		return True | ||||
| 	 | ||||
|  | ||||
| 	def subscribe(self, jid, node, bare=True, subscribee=None): | ||||
| 		pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') | ||||
| 		subscribe = ET.Element('subscribe') | ||||
| 		subscribe.attrib['node'] = node | ||||
| 		if subscribee is None: | ||||
| 			if bare: | ||||
| 				subscribe.attrib['jid'] = self.xmpp.jid | ||||
| 				subscribe.attrib['jid'] = self.xmpp.boundjid.bare | ||||
| 			else: | ||||
| 				subscribe.attrib['jid'] = self.xmpp.fulljid | ||||
| 				subscribe.attrib['jid'] = self.xmpp.boundjid.full | ||||
| 		else: | ||||
| 			subscribe.attrib['jid'] = subscribee | ||||
| 		pubsub.append(subscribe) | ||||
| 		iq = self.xmpp.makeIqSet(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		result = iq.send() | ||||
| 		if result is False or result is None or result['type'] == 'error': return False | ||||
| 		return True | ||||
| 	 | ||||
|  | ||||
| 	def unsubscribe(self, jid, node, bare=True, subscribee=None): | ||||
| 		pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') | ||||
| 		unsubscribe = ET.Element('unsubscribe') | ||||
| 		unsubscribe.attrib['node'] = node | ||||
| 		if subscribee is None: | ||||
| 			if bare: | ||||
| 				unsubscribe.attrib['jid'] = self.xmpp.jid | ||||
| 				unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare | ||||
| 			else: | ||||
| 				unsubscribe.attrib['jid'] = self.xmpp.fulljid | ||||
| 				unsubscribe.attrib['jid'] = self.xmpp.boundjid.full | ||||
| 		else: | ||||
| 			unsubscribe.attrib['jid'] = subscribee | ||||
| 		pubsub.append(unsubscribe) | ||||
| 		iq = self.xmpp.makeIqSet(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		result = iq.send() | ||||
| 		if result is False or result is None or result['type'] == 'error': return False | ||||
| 		return True | ||||
| 	 | ||||
|  | ||||
| 	def getNodeConfig(self, jid, node=None): # if no node, then grab default | ||||
| 		pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') | ||||
| 		if node is not None: | ||||
| @@ -105,22 +109,22 @@ class xep_0060(base.base_plugin): | ||||
| 		iq = self.xmpp.makeIqGet() | ||||
| 		iq.append(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse) | ||||
| 		result = iq.send() | ||||
| 		if result is None or result == False or result['type'] == 'error': | ||||
| 			logging.warning("got error instead of config") | ||||
| 			log.warning("got error instead of config") | ||||
| 			return False | ||||
| 		if node is not None: | ||||
| 			form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x') | ||||
| 		else: | ||||
| 			form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x') | ||||
| 		if not form or form is None: | ||||
| 			logging.error("No form found.") | ||||
| 			log.error("No form found.") | ||||
| 			return False | ||||
| 		return Form(xml=form) | ||||
| 	 | ||||
|  | ||||
| 	def getNodeSubscriptions(self, jid, node): | ||||
| 		pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') | ||||
| 		subscriptions = ET.Element('subscriptions') | ||||
| @@ -129,11 +133,11 @@ class xep_0060(base.base_plugin): | ||||
| 		iq = self.xmpp.makeIqGet() | ||||
| 		iq.append(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		result = iq.send() | ||||
| 		if result is None or result == False or result['type'] == 'error': | ||||
| 			logging.warning("got error instead of config") | ||||
| 			log.warning("got error instead of config") | ||||
| 			return False | ||||
| 		else: | ||||
| 			results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription') | ||||
| @@ -152,11 +156,11 @@ class xep_0060(base.base_plugin): | ||||
| 		iq = self.xmpp.makeIqGet() | ||||
| 		iq.append(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		result = iq.send() | ||||
| 		if result is None or result == False or result['type'] == 'error': | ||||
| 			logging.warning("got error instead of config") | ||||
| 			log.warning("got error instead of config") | ||||
| 			return False | ||||
| 		else: | ||||
| 			results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation') | ||||
| @@ -175,14 +179,14 @@ class xep_0060(base.base_plugin): | ||||
| 		pubsub.append(delete) | ||||
| 		iq.append(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		result = iq.send() | ||||
| 		if result is not None and result is not False and result['type'] != 'error': | ||||
| 			return True | ||||
| 		else: | ||||
| 			return False | ||||
| 		 | ||||
| 	 | ||||
|  | ||||
|  | ||||
| 	def setNodeConfig(self, jid, node, config): | ||||
| 		pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') | ||||
| 		configure = ET.Element('configure') | ||||
| @@ -192,13 +196,13 @@ class xep_0060(base.base_plugin): | ||||
| 		pubsub.append(configure) | ||||
| 		iq = self.xmpp.makeIqSet(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		result = iq.send() | ||||
| 		if result is None or result['type'] == 'error':  | ||||
| 		if result is None or result['type'] == 'error': | ||||
| 			return False | ||||
| 		return True | ||||
| 	 | ||||
|  | ||||
| 	def setItem(self, jid, node, items=[]): | ||||
| 		pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') | ||||
| 		publish = ET.Element('publish') | ||||
| @@ -213,12 +217,12 @@ class xep_0060(base.base_plugin): | ||||
| 		pubsub.append(publish) | ||||
| 		iq = self.xmpp.makeIqSet(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		result = iq.send() | ||||
| 		if result is None or result is False or result['type'] == 'error': return False | ||||
| 		return True | ||||
| 	 | ||||
|  | ||||
| 	def addItem(self, jid, node, items=[]): | ||||
| 		return self.setItem(jid, node, items) | ||||
|  | ||||
| @@ -232,12 +236,12 @@ class xep_0060(base.base_plugin): | ||||
| 		pubsub.append(retract) | ||||
| 		iq = self.xmpp.makeIqSet(pubsub) | ||||
| 		iq.attrib['to'] = jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		result = iq.send() | ||||
| 		if result is None or result is False or result['type'] == 'error': return False | ||||
| 		return True | ||||
| 	 | ||||
|  | ||||
| 	def getNodes(self, jid): | ||||
| 		response = self.xmpp.plugin['xep_0030'].getItems(jid) | ||||
| 		items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') | ||||
| @@ -246,7 +250,7 @@ class xep_0060(base.base_plugin): | ||||
| 			for item in items: | ||||
| 				nodes[item.get('node')] = item.get('name') | ||||
| 		return nodes | ||||
| 	 | ||||
|  | ||||
| 	def getItems(self, jid, node): | ||||
| 		response = self.xmpp.plugin['xep_0030'].getItems(jid, node) | ||||
| 		items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') | ||||
| @@ -264,7 +268,7 @@ class xep_0060(base.base_plugin): | ||||
| 		try: | ||||
| 			config.field['pubsub#collection'].setValue(parent) | ||||
| 		except KeyError: | ||||
| 			logging.warning("pubsub#collection doesn't exist in config, trying to add it") | ||||
| 			log.warning("pubsub#collection doesn't exist in config, trying to add it") | ||||
| 			config.addField('pubsub#collection', value=parent) | ||||
| 		if not self.setNodeConfig(jid, child, config): | ||||
| 			return False | ||||
| @@ -283,7 +287,7 @@ class xep_0060(base.base_plugin): | ||||
| 		pubsub.append(affs) | ||||
| 		iq = self.xmpp.makeIqSet(pubsub) | ||||
| 		iq.attrib['to'] = ps_jid | ||||
| 		iq.attrib['from'] = self.xmpp.fulljid | ||||
| 		iq.attrib['from'] = self.xmpp.boundjid.full | ||||
| 		id = iq['id'] | ||||
| 		result = iq.send() | ||||
| 		if result is None or result is False or result['type'] == 'error': | ||||
| @@ -298,7 +302,7 @@ class xep_0060(base.base_plugin): | ||||
| 		try: | ||||
| 			config.field['pubsub#collection'].setValue(parent) | ||||
| 		except KeyError: | ||||
| 			logging.warning("pubsub#collection doesn't exist in config, trying to add it") | ||||
| 			log.warning("pubsub#collection doesn't exist in config, trying to add it") | ||||
| 			config.addField('pubsub#collection', value=parent) | ||||
| 		if not self.setNodeConfig(jid, child, config): | ||||
| 			return False | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|     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 __future__ import with_statement | ||||
| @@ -12,6 +12,9 @@ import hashlib | ||||
| from . import base | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class xep_0078(base.base_plugin): | ||||
| 	""" | ||||
| 	XEP-0078 NON-SASL Authentication | ||||
| @@ -23,17 +26,17 @@ class xep_0078(base.base_plugin): | ||||
| 		#disabling until I fix conflict with PLAIN | ||||
| 		#self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth) | ||||
| 		self.streamid = '' | ||||
| 	 | ||||
|  | ||||
| 	def check_stream(self, xml): | ||||
| 		self.streamid = xml.attrib['id'] | ||||
| 		if xml.get('version', '0') != '1.0': | ||||
| 			self.auth() | ||||
| 	 | ||||
|  | ||||
| 	def auth(self, xml=None): | ||||
| 		logging.debug("Starting jabber:iq:auth Authentication") | ||||
| 		log.debug("Starting jabber:iq:auth Authentication") | ||||
| 		auth_request = self.xmpp.makeIqGet() | ||||
| 		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.text = self.xmpp.username | ||||
| 		auth_request_query.append(username) | ||||
| @@ -47,12 +50,12 @@ class xep_0078(base.base_plugin): | ||||
| 		query.append(username) | ||||
| 		query.append(resource) | ||||
| 		if rquery.find('{jabber:iq:auth}digest') is None: | ||||
| 			logging.warning("Authenticating via jabber:iq:auth Plain.") | ||||
| 			log.warning("Authenticating via jabber:iq:auth Plain.") | ||||
| 			password = ET.Element('password') | ||||
| 			password.text = self.xmpp.password | ||||
| 			query.append(password) | ||||
| 		else: | ||||
| 			logging.debug("Authenticating via jabber:iq:auth Digest") | ||||
| 			log.debug("Authenticating via jabber:iq:auth Digest") | ||||
| 			digest = ET.Element('digest') | ||||
| 			digest.text = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest() | ||||
| 			query.append(digest) | ||||
| @@ -64,6 +67,6 @@ class xep_0078(base.base_plugin): | ||||
| 				self.xmpp.sessionstarted = True | ||||
| 			self.xmpp.event("session_start") | ||||
| 		else: | ||||
| 			logging.info("Authentication failed") | ||||
| 			log.info("Authentication failed") | ||||
| 			self.xmpp.disconnect() | ||||
| 			self.xmpp.event("failed_auth") | ||||
|   | ||||
| @@ -1,101 +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 | ||||
|  | ||||
|  | ||||
| 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 | ||||
|         logging.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,59 +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 | ||||
|  | ||||
| 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='http://www.xmpp.org/extensions/xep-0199.html#ns'/></iq>" % self.xmpp.default_ns, self.handler_ping, name='XMPP Ping') | ||||
| 		self.running = False | ||||
| 		#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('http://www.xmpp.org/extensions/xep-0199.html#ns') | ||||
| 	 | ||||
| 	def handler_pingserver(self, xml): | ||||
| 		if not self.running: | ||||
| 			time.sleep(self.config.get('frequency', 300)) | ||||
| 			while self.sendPing(self.xmpp.server, self.config.get('timeout', 30)) is not False: | ||||
| 				time.sleep(self.config.get('frequency', 300)) | ||||
| 			logging.debug("Did not recieve ping back in time.  Requesting Reconnect.") | ||||
| 			self.xmpp.disconnect(reconnect=True) | ||||
| 	 | ||||
| 	def handler_ping(self, xml): | ||||
| 		iq = self.xmpp.makeIqResult(xml.get('id', 'unknown')) | ||||
| 		iq.attrib['to'] = xml.get('from', self.xmpp.server) | ||||
| 		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('{http://www.xmpp.org/extensions/xep-0199.html#ns}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() | ||||
							
								
								
									
										117
									
								
								sleekxmpp/plugins/xep_0202.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								sleekxmpp/plugins/xep_0202.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| """ | ||||
|     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 datetime import datetime, tzinfo | ||||
| import logging | ||||
| import time | ||||
|  | ||||
| from . import base | ||||
| from .. stanza.iq import Iq | ||||
| from .. xmlstream.handler.callback import Callback | ||||
| from .. xmlstream.matcher.xpath import MatchXPath | ||||
| from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class EntityTime(ElementBase): | ||||
|     name = 'time' | ||||
|     namespace = 'urn:xmpp:time' | ||||
|     plugin_attrib = 'entity_time' | ||||
|     interfaces = set(('tzo', 'utc')) | ||||
|     sub_interfaces = set(('tzo', 'utc')) | ||||
|  | ||||
|     #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 | ||||
|  | ||||
|     def set_tzo(self, tzo): | ||||
|         if isinstance(tzo, tzinfo): | ||||
|             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 | ||||
|             sign = ('+' if seconds >= 0 else '-') | ||||
|             minutes = abs(seconds // 60) | ||||
|             tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60) | ||||
|         elif not isinstance(tzo, str): | ||||
|             raise TypeError('The time should be a string or a datetime.tzinfo object.') | ||||
|         self._set_sub_text('tzo', tzo) | ||||
|  | ||||
|     def get_utc(self): | ||||
|         # Returns a datetime object instead the string. Is this a good idea? | ||||
|         value = self._get_sub_text('utc') | ||||
|         if '.' in value: | ||||
|             return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ') | ||||
|         else: | ||||
|             return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') | ||||
|  | ||||
|     def set_utc(self, tim=None): | ||||
|         if isinstance(tim, datetime): | ||||
|             if tim.utcoffset(): | ||||
|                 tim = tim - tim.utcoffset() | ||||
|             tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ') | ||||
|         elif isinstance(tim, time.struct_time): | ||||
|             tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim) | ||||
|         elif not isinstance(tim, str): | ||||
|             raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.') | ||||
|  | ||||
|         self._set_sub_text('utc', tim) | ||||
|  | ||||
|  | ||||
| class xep_0202(base.base_plugin): | ||||
|     """ | ||||
|     XEP-0202 Entity Time | ||||
|     """ | ||||
|     def plugin_init(self): | ||||
|         self.description = "Entity Time" | ||||
|         self.xep = "0202" | ||||
|  | ||||
|         self.xmpp.registerHandler( | ||||
|             Callback('Time Request', | ||||
|                  MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns, | ||||
|                                   EntityTime.namespace)), | ||||
|                  self.handle_entity_time_query)) | ||||
|         register_stanza_plugin(Iq, EntityTime) | ||||
|  | ||||
|         self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time) | ||||
|  | ||||
|  | ||||
|     def post_init(self): | ||||
|         base.base_plugin.post_init(self) | ||||
|  | ||||
|         self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time') | ||||
|  | ||||
|     def handle_entity_time_query(self, iq): | ||||
|         if iq['type'] == 'get': | ||||
|             log.debug("Entity time requested by %s" % iq['from']) | ||||
|             self.xmpp.event('entity_time_request', iq) | ||||
|         elif iq['type'] == 'result': | ||||
|             log.debug("Entity time result from %s" % iq['from']) | ||||
|             self.xmpp.event('entity_time', iq) | ||||
|  | ||||
|     def handle_entity_time(self, iq): | ||||
|         iq = iq.reply() | ||||
|         iq.enable('entity_time') | ||||
|         tzo = time.strftime('%z') # %z is not on all ANSI C libraries | ||||
|         tzo = tzo[:3] + ':' + tzo[3:] | ||||
|         iq['entity_time']['tzo'] = tzo | ||||
|         iq['entity_time']['utc'] = datetime.utcnow() | ||||
|         iq.send() | ||||
|  | ||||
|     def get_entity_time(self, jid): | ||||
|         iq = self.xmpp.makeIqGet() | ||||
|         iq.enable('entity_time') | ||||
|         iq.attrib['to'] = jid | ||||
|         iq.attrib['from'] = self.xmpp.boundjid.full | ||||
|         id = iq.get('id') | ||||
|         result = iq.send() | ||||
|         if result and result is not None and result.get('type', 'error') != 'error': | ||||
|             return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']} | ||||
|         else: | ||||
|             return False | ||||
							
								
								
									
										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.stream_error import StreamError | ||||
| from sleekxmpp.stanza.iq import Iq | ||||
| from sleekxmpp.stanza.message import Message | ||||
| from sleekxmpp.stanza.presence import Presence | ||||
|   | ||||
| @@ -77,15 +77,6 @@ class Error(ElementBase): | ||||
|         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.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 we had to generate XML then set default values. | ||||
|             self['type'] = 'cancel' | ||||
| @@ -139,3 +130,13 @@ class Error(ElementBase): | ||||
|         """Remove the <text> element.""" | ||||
|         self._del_sub('{%s}text' % self.condition_ns) | ||||
|         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',)) | ||||
|     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): | ||||
|         """ | ||||
|         Set the contents of the HTML body. | ||||
| @@ -95,3 +78,9 @@ class HTMLIM(ElementBase): | ||||
|  | ||||
|  | ||||
| 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.rootstanza import RootStanza | ||||
| from sleekxmpp.xmlstream import RESPONSE_TIMEOUT, StanzaBase, ET | ||||
| from sleekxmpp.xmlstream.handler import Waiter | ||||
| from sleekxmpp.xmlstream import StanzaBase, ET | ||||
| from sleekxmpp.xmlstream.handler import Waiter, Callback | ||||
| from sleekxmpp.xmlstream.matcher import MatcherId | ||||
|  | ||||
|  | ||||
| @@ -75,16 +75,9 @@ class Iq(RootStanza): | ||||
|         Overrides StanzaBase.__init__. | ||||
|         """ | ||||
|         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.stream is not None: | ||||
|                 self['id'] = self.stream.getNewId() | ||||
|                 self['id'] = self.stream.new_id() | ||||
|             else: | ||||
|                 self['id'] = '0' | ||||
|  | ||||
| @@ -144,7 +137,7 @@ class Iq(RootStanza): | ||||
|                 self.xml.remove(child) | ||||
|         return self | ||||
|  | ||||
|     def reply(self): | ||||
|     def reply(self, clear=True): | ||||
|         """ | ||||
|         Send a reply <iq> stanza. | ||||
|  | ||||
| @@ -152,32 +145,91 @@ class Iq(RootStanza): | ||||
|  | ||||
|         Sets the 'type' to 'result' in addition to the default | ||||
|         StanzaBase.reply behavior. | ||||
|  | ||||
|         Arguments: | ||||
|             clear -- Indicates if existing content should be | ||||
|                      removed before replying. Defaults to True. | ||||
|         """ | ||||
|         self['type'] = 'result' | ||||
|         StanzaBase.reply(self) | ||||
|         StanzaBase.reply(self, clear) | ||||
|         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. | ||||
|  | ||||
|         The send call can optionally block until a response is received or | ||||
|         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 | ||||
|  | ||||
|         Arguments: | ||||
|             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 | ||||
|             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. | ||||
|             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'])) | ||||
|             self.stream.registerHandler(waitfor) | ||||
|             StanzaBase.send(self) | ||||
|             self.stream.register_handler(waitfor) | ||||
|             StanzaBase.send(self, now=now) | ||||
|             return waitfor.wait(timeout) | ||||
|         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 | ||||
|     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): | ||||
|         """ | ||||
|         Return the message type. | ||||
| @@ -104,7 +83,7 @@ class Message(RootStanza): | ||||
|         self['type'] = 'normal' | ||||
|         return self | ||||
|  | ||||
|     def reply(self, body=None): | ||||
|     def reply(self, body=None, clear=True): | ||||
|         """ | ||||
|         Create a message reply. | ||||
|  | ||||
| @@ -114,9 +93,11 @@ class Message(RootStanza): | ||||
|         adds a message body if one is given. | ||||
|  | ||||
|         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': | ||||
|             self['to'] = self['to'].bare | ||||
|  | ||||
| @@ -163,3 +144,14 @@ class Message(RootStanza): | ||||
|     def del_mucnick(self): | ||||
|         """Dummy method to prevent deletion.""" | ||||
|         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. | ||||
|     """ | ||||
|  | ||||
|     namespace = 'http://jabber.org/nick/nick' | ||||
|     namespace = 'http://jabber.org/protocol/nick' | ||||
|     name = 'nick' | ||||
|     plugin_attrib = name | ||||
|     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): | ||||
|         """ | ||||
|         Add a <nick> element with the given nickname. | ||||
| @@ -87,3 +70,9 @@ class Nick(ElementBase): | ||||
|  | ||||
| register_stanza_plugin(Message, 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,25 +72,11 @@ class Presence(RootStanza): | ||||
|                  'subscribed', 'unsubscribe', 'unsubscribed')) | ||||
|     showtypes = set(('dnd', 'chat', 'xa', 'away')) | ||||
|  | ||||
|     def setup(self, xml=None): | ||||
|     def exception(self, e): | ||||
|         """ | ||||
|         Populate the stanza object using an optional XML object. | ||||
|  | ||||
|         Overrides ElementBase.setup. | ||||
|  | ||||
|         Arguments: | ||||
|             xml -- Use an existing XML object for the stanza's values. | ||||
|         Override exception passback for presence. | ||||
|         """ | ||||
|         # 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) | ||||
|         pass | ||||
|  | ||||
|     def set_show(self, show): | ||||
|         """ | ||||
| @@ -167,14 +153,28 @@ class Presence(RootStanza): | ||||
|             # The priority is not a number: we consider it 0 as a default | ||||
|             return 0 | ||||
|  | ||||
|     def reply(self): | ||||
|     def reply(self, clear=True): | ||||
|         """ | ||||
|         Set the appropriate presence reply type. | ||||
|  | ||||
|         Overrides StanzaBase.reply. | ||||
|  | ||||
|         Arguments: | ||||
|             clear -- Indicates if the stanza contents should be removed | ||||
|                      before replying. Defaults to True. | ||||
|         """ | ||||
|         if self['type'] == 'unsubscribe': | ||||
|             self['type'] = 'unsubscribed' | ||||
|         elif self['type'] == 'subscribe': | ||||
|             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 | ||||
|   | ||||
| @@ -15,6 +15,9 @@ from sleekxmpp.stanza import Error | ||||
| from sleekxmpp.xmlstream import ET, StanzaBase, register_stanza_plugin | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class RootStanza(StanzaBase): | ||||
|  | ||||
|     """ | ||||
| @@ -40,8 +43,8 @@ class RootStanza(StanzaBase): | ||||
|         Arguments: | ||||
|             e -- Exception object | ||||
|         """ | ||||
|         self.reply() | ||||
|         if isinstance(e, XMPPError): | ||||
|             self.reply(clear=e.clear) | ||||
|             # We raised this deliberately | ||||
|             self['error']['condition'] = e.condition | ||||
|             self['error']['text'] = e.text | ||||
| @@ -51,16 +54,18 @@ class RootStanza(StanzaBase): | ||||
|                                     e.extension_args) | ||||
|                 self['error'].append(extxml) | ||||
|                 self['error']['type'] = e.etype | ||||
|             self.send() | ||||
|         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' | ||||
|             if sys.version_info < (3, 0): | ||||
|                 self['error']['text'] = "SleekXMPP got into trouble." | ||||
|             else: | ||||
|                 self['error']['text'] = traceback.format_tb(e.__traceback__) | ||||
|                 logging.exception('Error handling {%s}%s stanza' % | ||||
|                                   (self.namespace, self.name)) | ||||
|         self.send() | ||||
|  | ||||
|             self['error']['text'] = "SleekXMPP got into trouble." | ||||
|             self.send() | ||||
|             # log the error | ||||
|             log.exception('Error handling {%s}%s stanza' % | ||||
|                           (self.namespace, self.name)) | ||||
|             # Finally raise the exception, so it can be handled (or not) | ||||
|             # at a higher level by using sys.excepthook. | ||||
|             raise e | ||||
|  | ||||
| register_stanza_plugin(RootStanza, Error) | ||||
|   | ||||
| @@ -38,23 +38,6 @@ class Roster(ElementBase): | ||||
|     plugin_attrib = 'roster' | ||||
|     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): | ||||
|         """ | ||||
|         Set the roster entries in the <roster> stanza. | ||||
| @@ -123,3 +106,9 @@ class Roster(ElementBase): | ||||
|  | ||||
|  | ||||
| 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 threading | ||||
| try: | ||||
|     import queue | ||||
| except ImportError: | ||||
| @@ -40,6 +41,8 @@ class TestLiveSocket(object): | ||||
|         self.recv_buffer = [] | ||||
|         self.recv_queue = queue.Queue() | ||||
|         self.send_queue = queue.Queue() | ||||
|         self.send_queue_lock = threading.Lock() | ||||
|         self.recv_queue_lock = threading.Lock() | ||||
|         self.is_live = True | ||||
|  | ||||
|     def __getattr__(self, name): | ||||
| @@ -55,6 +58,18 @@ class TestLiveSocket(object): | ||||
|     # ------------------------------------------------------------------ | ||||
|     # 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): | ||||
|         """ | ||||
|         Get the next stanza that has been sent. | ||||
| @@ -108,7 +123,8 @@ class TestLiveSocket(object): | ||||
|             Placeholders. Same as for socket.recv. | ||||
|         """ | ||||
|         data = self.socket.recv(*args, **kwargs) | ||||
|         self.recv_queue.put(data) | ||||
|         with self.recv_queue_lock: | ||||
|             self.recv_queue.put(data) | ||||
|         return data | ||||
|  | ||||
|     def send(self, data): | ||||
| @@ -120,7 +136,8 @@ class TestLiveSocket(object): | ||||
|         Arguments: | ||||
|             data -- String value to write. | ||||
|         """ | ||||
|         self.send_queue.put(data) | ||||
|         with self.send_queue_lock: | ||||
|             self.send_queue.put(data) | ||||
|         self.socket.send(data) | ||||
|  | ||||
|     # ------------------------------------------------------------------ | ||||
| @@ -143,3 +160,15 @@ class TestLiveSocket(object): | ||||
|             Placeholders, same as socket.recv() | ||||
|         """ | ||||
|         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.send_queue = queue.Queue() | ||||
|         self.is_live = False | ||||
|         self.disconnected = False | ||||
|  | ||||
|     def __getattr__(self, name): | ||||
|         """ | ||||
| @@ -89,6 +90,13 @@ class TestSocket(object): | ||||
|         """ | ||||
|         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 | ||||
|  | ||||
| @@ -99,6 +107,8 @@ class TestSocket(object): | ||||
|         Arguments: | ||||
|             Placeholders. Same as for socket.Socket.recv. | ||||
|         """ | ||||
|         if self.disconnected: | ||||
|             raise socket.error | ||||
|         return self.read(block=True) | ||||
|  | ||||
|     def send(self, data): | ||||
| @@ -108,6 +118,8 @@ class TestSocket(object): | ||||
|         Arguments: | ||||
|             data -- String value to write. | ||||
|         """ | ||||
|         if self.disconnected: | ||||
|             raise socket.error | ||||
|         self.send_queue.put(data) | ||||
|  | ||||
|     # ------------------------------------------------------------------ | ||||
| @@ -132,6 +144,8 @@ class TestSocket(object): | ||||
|             timeout -- Time in seconds a block should last before | ||||
|                        returning None. | ||||
|         """ | ||||
|         if self.disconnected: | ||||
|             raise socket.error | ||||
|         if timeout is not None: | ||||
|             block = True | ||||
|         try: | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| """ | ||||
|  | ||||
|     SleekXMPP: The Sleek XMPP Library | ||||
|     Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of SleekXMPP. | ||||
| @@ -8,13 +7,20 @@ | ||||
| """ | ||||
|  | ||||
| import unittest | ||||
| try: | ||||
|     import Queue as queue | ||||
| except: | ||||
|     import queue | ||||
|  | ||||
| import sleekxmpp | ||||
| from sleekxmpp import ClientXMPP, ComponentXMPP | ||||
| from sleekxmpp.stanza import Message, Iq, Presence | ||||
| 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.matcher import StanzaPath, MatcherId | ||||
| from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath | ||||
|  | ||||
|  | ||||
| class SleekTest(unittest.TestCase): | ||||
| @@ -27,27 +33,33 @@ class SleekTest(unittest.TestCase): | ||||
|         Message              -- Create a Message stanza object. | ||||
|         Iq                   -- Create an Iq stanza object. | ||||
|         Presence             -- Create a Presence stanza object. | ||||
|         check_stanza         -- Compare a generic stanza against an XML string. | ||||
|         check_message        -- Compare a Message stanza against an XML string. | ||||
|         check_iq             -- Compare an Iq stanza against an XML string. | ||||
|         check_presence       -- Compare a Presence stanza against an XML string. | ||||
|         check_jid            -- Check a JID and its component parts. | ||||
|         check                -- Compare a stanza against an XML string. | ||||
|         stream_start         -- Initialize a dummy XMPP client. | ||||
|         stream_recv          -- Queue data for XMPP client to receive. | ||||
|         stream_make_header   -- Create a stream header. | ||||
|         stream_send_header   -- Check that the given header has been sent. | ||||
|         stream_send_message  -- Check that the XMPP client sent the given | ||||
|                                 Message stanza. | ||||
|         stream_send_iq       -- Check that the XMPP client sent the given | ||||
|                                 Iq stanza. | ||||
|         stream_send_presence -- Check thatt the XMPP client sent the given | ||||
|                                 Presence stanza. | ||||
|         stream_send_stanza   -- Check that the XMPP client sent the given | ||||
|                                 generic stanza. | ||||
|         stream_close         -- Disconnect the XMPP client. | ||||
|         make_header          -- Create a stream header. | ||||
|         send_header          -- Check that the given header has been sent. | ||||
|         send_feature         -- Send a raw XML element. | ||||
|         send                 -- Check that the XMPP client sent the given | ||||
|                                 generic stanza. | ||||
|         recv                 -- Queue data for XMPP client to receive, or | ||||
|                                 verify the data that was received from a | ||||
|                                 live connection. | ||||
|         recv_header          -- Check that a given stream header | ||||
|                                 was received. | ||||
|         recv_feature         -- Check that a given, raw XML element | ||||
|                                 was recveived. | ||||
|         fix_namespaces       -- Add top-level namespace to an XML object. | ||||
|         compare              -- Compare XML objects against each other. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         unittest.TestCase.__init__(self, *args, **kwargs) | ||||
|         self.xmpp = None | ||||
|  | ||||
|     def runTest(self): | ||||
|         pass | ||||
|  | ||||
|     def parse_xml(self, xml_string): | ||||
|         try: | ||||
|             xml = ET.fromstring(xml_string) | ||||
| @@ -66,6 +78,8 @@ class SleekTest(unittest.TestCase): | ||||
|                 xml = self.parse_xml(xml_string) | ||||
|                 xml = xml.getchildren()[0] | ||||
|                 return xml | ||||
|             else: | ||||
|                 self.fail("XML data was mal-formed:\n%s" % xml_string) | ||||
|  | ||||
|     # ------------------------------------------------------------------ | ||||
|     # Shortcut methods for creating stanza objects | ||||
| @@ -79,7 +93,7 @@ class SleekTest(unittest.TestCase): | ||||
|         Arguments: | ||||
|             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): | ||||
|         """ | ||||
| @@ -90,7 +104,7 @@ class SleekTest(unittest.TestCase): | ||||
|         Arguments: | ||||
|             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): | ||||
|         """ | ||||
| @@ -101,18 +115,50 @@ class SleekTest(unittest.TestCase): | ||||
|         Arguments: | ||||
|             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, | ||||
|                   bare=None, full=None, string=None): | ||||
|         """ | ||||
|         Verify the components of a JID. | ||||
|  | ||||
|         Arguments: | ||||
|             jid      -- The JID object to test. | ||||
|             user     -- Optional. The user name portion of the JID. | ||||
|             domain   -- Optional. The domain name portion of the JID. | ||||
|             resource -- Optional. The resource portion of the JID. | ||||
|             bare     -- Optional. The bare JID. | ||||
|             full     -- Optional. The full JID. | ||||
|             string   -- Optional. The string version of the JID. | ||||
|         """ | ||||
|         if user is not None: | ||||
|             self.assertEqual(jid.user, user, | ||||
|                     "User does not match: %s" % jid.user) | ||||
|         if domain is not None: | ||||
|             self.assertEqual(jid.domain, domain, | ||||
|                     "Domain does not match: %s" % jid.domain) | ||||
|         if resource is not None: | ||||
|             self.assertEqual(jid.resource, resource, | ||||
|                     "Resource does not match: %s" % jid.resource) | ||||
|         if bare is not None: | ||||
|             self.assertEqual(jid.bare, bare, | ||||
|                     "Bare JID does not match: %s" % jid.bare) | ||||
|         if full is not None: | ||||
|             self.assertEqual(jid.full, full, | ||||
|                     "Full JID does not match: %s" % jid.full) | ||||
|         if string is not None: | ||||
|             self.assertEqual(str(jid), string, | ||||
|                     "String does not match: %s" % str(jid)) | ||||
|  | ||||
|     # ------------------------------------------------------------------ | ||||
|     # Methods for comparing stanza objects to XML strings | ||||
|  | ||||
|     def check_stanza(self, stanza_class, stanza, xml_string, | ||||
|                      defaults=None, use_values=True): | ||||
|     def check(self, stanza, criteria, method='exact', | ||||
|               defaults=None, use_values=True): | ||||
|         """ | ||||
|         Create and compare several stanza objects to a correct XML string. | ||||
|  | ||||
|         If use_values is False, test using getStanzaValues() and | ||||
|         setStanzaValues() will not be used. | ||||
|         If use_values is False, tests using stanza.values will not be used. | ||||
|  | ||||
|         Some stanzas provide default values for some interfaces, but | ||||
|         these defaults can be problematic for testing since they can easily | ||||
| @@ -126,127 +172,104 @@ class SleekTest(unittest.TestCase): | ||||
|         must take into account any extra elements that are included by default. | ||||
|  | ||||
|         Arguments: | ||||
|             stanza_class -- The class of the stanza being tested. | ||||
|             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 | ||||
|                             values. These interfaces will be set to their | ||||
|                             defaults for the given and generated stanzas to | ||||
|                             prevent unexpected test failures. | ||||
|             use_values   -- Indicates if testing using getStanzaValues() and | ||||
|                             setStanzaValues() should be used. Defaults to | ||||
|                             True. | ||||
|             use_values   -- Indicates if testing using stanza.values should | ||||
|                             be used. Defaults to True. | ||||
|         """ | ||||
|         xml = self.parse_xml(xml_string) | ||||
|         if method is None and hasattr(self, 'match_method'): | ||||
|             method = getattr(self, 'match_method') | ||||
|  | ||||
|         # 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 getStanzaValues() and setStanzaValues() 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: | ||||
|                 defaults = [] | ||||
|             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) | ||||
|         if method != 'exact': | ||||
|             matchers = {'stanzapath': StanzaPath, | ||||
|                         'xpath': MatchXPath, | ||||
|                         'mask': MatchXMLMask, | ||||
|                         'id': MatcherId} | ||||
|             Matcher = matchers.get(method, None) | ||||
|             if Matcher is None: | ||||
|                 raise ValueError("Unknown matching method.") | ||||
|             test = Matcher(criteria) | ||||
|             self.failUnless(test.match(stanza), | ||||
|                     "Stanza did not match using %s method:\n" % method + \ | ||||
|                     "Criteria:\n%s\n" % str(criteria) + \ | ||||
|                     "Stanza:\n%s" % str(stanza)) | ||||
|         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) | ||||
|             stanza_class = stanza.__class__ | ||||
|             if not isinstance(criteria, ElementBase): | ||||
|                 xml = self.parse_xml(criteria) | ||||
|             else: | ||||
|                 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') | ||||
|  | ||||
|     def check_message(self, msg, xml_string, use_values=True): | ||||
|         """ | ||||
|         Create and compare several message stanza objects to a | ||||
|         correct XML string. | ||||
|             stanza2 = stanza_class(xml=xml) | ||||
|  | ||||
|         If use_values is False, the test using getStanzaValues() and | ||||
|         setStanzaValues() will not be used. | ||||
|             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 | ||||
|  | ||||
|         Arguments: | ||||
|             msg        -- The Message stanza object to check. | ||||
|             xml_string -- The XML contents to compare against. | ||||
|             use_values -- Indicates if the test using getStanzaValues | ||||
|                           and setStanzaValues should be used. Defaults | ||||
|                           to True. | ||||
|         """ | ||||
|                 values = stanza2.values | ||||
|                 stanza3 = stanza_class() | ||||
|                 stanza3.values = values | ||||
|  | ||||
|         return self.check_stanza(Message, msg, xml_string, | ||||
|                                  defaults=['type'], | ||||
|                                  use_values=use_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) | ||||
|  | ||||
|     def check_iq(self, iq, xml_string, use_values=True): | ||||
|         """ | ||||
|         Create and compare several iq stanza objects to a | ||||
|         correct XML string. | ||||
|  | ||||
|         If use_values is False, the test using getStanzaValues() and | ||||
|         setStanzaValues() will not be used. | ||||
|  | ||||
|         Arguments: | ||||
|             iq         -- The Iq stanza object to check. | ||||
|             xml_string -- The XML contents to compare against. | ||||
|             use_values -- Indicates if the test using getStanzaValues | ||||
|                           and setStanzaValues should be used. Defaults | ||||
|                           to True. | ||||
|         """ | ||||
|         return self.check_stanza(Iq, iq, xml_string, use_values=use_values) | ||||
|  | ||||
|     def check_presence(self, pres, xml_string, use_values=True): | ||||
|         """ | ||||
|         Create and compare several presence stanza objects to a | ||||
|         correct XML string. | ||||
|  | ||||
|         If use_values is False, the test using getStanzaValues() and | ||||
|         setStanzaValues() will not be used. | ||||
|  | ||||
|         Arguments: | ||||
|             iq         -- The Iq stanza object to check. | ||||
|             xml_string -- The XML contents to compare against. | ||||
|             use_values -- Indicates if the test using getStanzaValues | ||||
|                           and setStanzaValues should be used. Defaults | ||||
|                           to True. | ||||
|         """ | ||||
|         return self.check_stanza(Presence, pres, xml_string, | ||||
|                                  defaults=['priority'], | ||||
|                                  use_values=use_values) | ||||
|             self.failUnless(result, debug) | ||||
|  | ||||
|     # ------------------------------------------------------------------ | ||||
|     # 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, | ||||
|                            socket='mock', jid='tester@localhost', | ||||
|                            password='test', server='localhost', | ||||
|                            port=5222): | ||||
|                            port=5222, plugins=None): | ||||
|         """ | ||||
|         Initialize an XMPP client or component using a dummy XML stream. | ||||
|  | ||||
| @@ -266,8 +289,9 @@ class SleekTest(unittest.TestCase): | ||||
|             server   -- The name of the XMPP server. Defaults to 'localhost'. | ||||
|             port     -- The port to use when connecting to the server. | ||||
|                         Defaults to 5222. | ||||
|             plugins  -- List of plugins to register. By default, all plugins | ||||
|                         are loaded. | ||||
|         """ | ||||
|  | ||||
|         if mode == 'client': | ||||
|             self.xmpp = ClientXMPP(jid, password) | ||||
|         elif mode == 'component': | ||||
| @@ -276,6 +300,10 @@ class SleekTest(unittest.TestCase): | ||||
|         else: | ||||
|             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': | ||||
|             self.xmpp.set_socket(TestSocket()) | ||||
|  | ||||
| @@ -290,24 +318,38 @@ class SleekTest(unittest.TestCase): | ||||
|             self.xmpp.socket.recv_data(header) | ||||
|         elif socket == 'live': | ||||
|             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() | ||||
|         else: | ||||
|             raise ValueError("Unknown socket type.") | ||||
|  | ||||
|         if plugins is None: | ||||
|             self.xmpp.register_plugins() | ||||
|         else: | ||||
|             for plugin in plugins: | ||||
|                 self.xmpp.register_plugin(plugin) | ||||
|         self.xmpp.process(threaded=True) | ||||
|         if skip: | ||||
|             # Clear startup stanzas | ||||
|             self.xmpp.socket.next_sent(timeout=1) | ||||
|             if mode == 'component': | ||||
|             if socket != 'live': | ||||
|                 # Mark send queue as usable | ||||
|                 self.xmpp.session_started_event.set() | ||||
|                 # Clear startup stanzas | ||||
|                 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 stream_make_header(self, sto='', | ||||
|                                  sfrom='', | ||||
|                                  sid='', | ||||
|                                  stream_ns="http://etherx.jabber.org/streams", | ||||
|                                  default_ns="jabber:client", | ||||
|                                  version="1.0", | ||||
|                                  xml_header=True): | ||||
|     def make_header(self, sto='', | ||||
|                           sfrom='', | ||||
|                           sid='', | ||||
|                           stream_ns="http://etherx.jabber.org/streams", | ||||
|                           default_ns="jabber:client", | ||||
|                           version="1.0", | ||||
|                           xml_header=True): | ||||
|         """ | ||||
|         Create a stream header to be received by the test XMPP agent. | ||||
|  | ||||
| @@ -338,23 +380,25 @@ class SleekTest(unittest.TestCase): | ||||
|         parts.append('xmlns="%s"' % default_ns) | ||||
|         return header % ' '.join(parts) | ||||
|  | ||||
|     def stream_recv(self, data, stanza_class=StanzaBase, defaults=[], | ||||
|                     use_values=True, timeout=1): | ||||
|     def recv(self, data, defaults=[], method='exact', | ||||
|              use_values=True, timeout=1): | ||||
|         """ | ||||
|         Pass data to the dummy XMPP client as if it came from an XMPP server. | ||||
|  | ||||
|         If using a live connection, verify what the server has sent. | ||||
|  | ||||
|         Arguments: | ||||
|             data         -- String stanza XML to be received and processed by | ||||
|                             the XMPP client or component. | ||||
|             stanza_class -- The stanza object class for verifying data received | ||||
|                             by a live connection. Defaults to StanzaBase. | ||||
|             data         -- If a dummy socket is being used, the XML that is to | ||||
|                             be received next. Otherwise it is the criteria used | ||||
|                             to match against live data that is received. | ||||
|             defaults     -- A list of stanza interfaces with default values that | ||||
|                             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 | ||||
|                             getStanzaValues() and setStanzaValues(). | ||||
|                             Defaults to True. | ||||
|                             stanza.values. Defaults to True. | ||||
|             timeout      -- Time to wait in seconds for data to be received by | ||||
|                             a live connection. | ||||
|         """ | ||||
| @@ -364,24 +408,27 @@ class SleekTest(unittest.TestCase): | ||||
|             # receiving data. | ||||
|             recv_data = self.xmpp.socket.next_recv(timeout) | ||||
|             if recv_data is None: | ||||
|                 return False | ||||
|             stanza = stanza_class(xml=self.parse_xml(recv_data)) | ||||
|             return self.check_stanza(stanza_class, stanza, data, | ||||
|                                      defaults=defaults, | ||||
|                                      use_values=use_values) | ||||
|                 self.fail("No stanza was received.") | ||||
|             xml = self.parse_xml(recv_data) | ||||
|             self.fix_namespaces(xml, 'jabber:client') | ||||
|             stanza = self.xmpp._build_stanza(xml, 'jabber:client') | ||||
|             self.check(stanza, data, | ||||
|                        method=method, | ||||
|                        defaults=defaults, | ||||
|                        use_values=use_values) | ||||
|         else: | ||||
|             # place the data in the dummy socket receiving queue. | ||||
|             data = str(data) | ||||
|             self.xmpp.socket.recv_data(data) | ||||
|  | ||||
|     def stream_recv_header(self, sto='', | ||||
|                                  sfrom='', | ||||
|                                  sid='', | ||||
|                                  stream_ns="http://etherx.jabber.org/streams", | ||||
|                                  default_ns="jabber:client", | ||||
|                                  version="1.0", | ||||
|                                  xml_header=False, | ||||
|                                  timeout=1): | ||||
|     def recv_header(self, sto='', | ||||
|                           sfrom='', | ||||
|                           sid='', | ||||
|                           stream_ns="http://etherx.jabber.org/streams", | ||||
|                           default_ns="jabber:client", | ||||
|                           version="1.0", | ||||
|                           xml_header=False, | ||||
|                           timeout=1): | ||||
|         """ | ||||
|         Check that a given stream header was received. | ||||
|  | ||||
| @@ -397,11 +444,11 @@ class SleekTest(unittest.TestCase): | ||||
|             timeout    -- Length of time to wait in seconds for a | ||||
|                           response. | ||||
|         """ | ||||
|         header = self.stream_make_header(sto, sfrom, sid, | ||||
|                                          stream_ns=stream_ns, | ||||
|                                          default_ns=default_ns, | ||||
|                                          version=version, | ||||
|                                          xml_header=xml_header) | ||||
|         header = self.make_header(sto, sfrom, sid, | ||||
|                                   stream_ns=stream_ns, | ||||
|                                   default_ns=default_ns, | ||||
|                                   version=version, | ||||
|                                   xml_header=xml_header) | ||||
|         recv_header = self.xmpp.socket.next_recv(timeout) | ||||
|         if recv_header is None: | ||||
|             raise ValueError("Socket did not return data.") | ||||
| @@ -441,61 +488,47 @@ class SleekTest(unittest.TestCase): | ||||
|             "Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % ( | ||||
|                 '%s %s' % (xml.tag, xml.attrib), | ||||
|                 '%s %s' % (recv_xml.tag, recv_xml.attrib))) | ||||
|                 #tostring(xml), tostring(recv_xml)))#recv_header)) | ||||
|  | ||||
|     def stream_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: | ||||
|             # we are working with a live connection, so we should | ||||
|             # verify what has been received instead of simulating | ||||
|             # receiving data. | ||||
|             recv_data = self.xmpp.socket.next_recv(timeout) | ||||
|             if recv_data is None: | ||||
|                 return False | ||||
|             xml = self.parse_xml(data) | ||||
|             recv_xml = self.parse_xml(recv_data) | ||||
|             self.failUnless(self.compare(xml, recv_xml), | ||||
|                 "Features do not match.\nDesired:\n%s\nReceived:\n%s" % ( | ||||
|                     tostring(xml), tostring(recv_xml))) | ||||
|             if recv_data is None: | ||||
|                 self.fail("No stanza was received.") | ||||
|             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: | ||||
|             # place the data in the dummy socket receiving queue. | ||||
|             data = str(data) | ||||
|             self.xmpp.socket.recv_data(data) | ||||
|  | ||||
|  | ||||
|  | ||||
|     def stream_recv_message(self, data, use_values=True, timeout=1): | ||||
|         """ | ||||
|         """ | ||||
|         return self.stream_recv(data, stanza_class=Message, | ||||
|                                       defaults=['type'], | ||||
|                                       use_values=use_values, | ||||
|                                       timeout=timeout) | ||||
|  | ||||
|     def stream_recv_iq(self, data, use_values=True, timeout=1): | ||||
|         """ | ||||
|         """ | ||||
|         return self.stream_recv(data, stanza_class=Iq, | ||||
|                                       use_values=use_values, | ||||
|                                       timeout=timeout) | ||||
|  | ||||
|     def stream_recv_presence(self, data, use_values=True, timeout=1): | ||||
|         """ | ||||
|         """ | ||||
|         return self.stream_recv(data, stanza_class=Presence, | ||||
|                                       defaults=['priority'], | ||||
|                                       use_values=use_values, | ||||
|                                       timeout=timeout) | ||||
|  | ||||
|     def stream_send_header(self, sto='', | ||||
|                                  sfrom='', | ||||
|                                  sid='', | ||||
|                                  stream_ns="http://etherx.jabber.org/streams", | ||||
|                                  default_ns="jabber:client", | ||||
|                                  version="1.0", | ||||
|                                  xml_header=False, | ||||
|                                  timeout=1): | ||||
|     def send_header(self, sto='', | ||||
|                           sfrom='', | ||||
|                           sid='', | ||||
|                           stream_ns="http://etherx.jabber.org/streams", | ||||
|                           default_ns="jabber:client", | ||||
|                           version="1.0", | ||||
|                           xml_header=False, | ||||
|                           timeout=1): | ||||
|         """ | ||||
|         Check that a given stream header was sent. | ||||
|  | ||||
| @@ -511,11 +544,11 @@ class SleekTest(unittest.TestCase): | ||||
|             timeout    -- Length of time to wait in seconds for a | ||||
|                           response. | ||||
|         """ | ||||
|         header = self.stream_make_header(sto, sfrom, sid, | ||||
|                                          stream_ns=stream_ns, | ||||
|                                          default_ns=default_ns, | ||||
|                                          version=version, | ||||
|                                          xml_header=xml_header) | ||||
|         header = self.make_header(sto, sfrom, sid, | ||||
|                                   stream_ns=stream_ns, | ||||
|                                   default_ns=default_ns, | ||||
|                                   version=version, | ||||
|                                   xml_header=xml_header) | ||||
|         sent_header = self.xmpp.socket.next_sent(timeout) | ||||
|         if sent_header is None: | ||||
|             raise ValueError("Socket did not return data.") | ||||
| @@ -533,25 +566,34 @@ class SleekTest(unittest.TestCase): | ||||
|             "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % ( | ||||
|                 header, sent_header)) | ||||
|  | ||||
|     def stream_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) | ||||
|         if sent_data is None: | ||||
|             return False | ||||
|         xml = self.parse_xml(data) | ||||
|         sent_xml = self.parse_xml(sent_data) | ||||
|         self.failUnless(self.compare(xml, sent_xml), | ||||
|             "Features do not match.\nDesired:\n%s\nSent:\n%s" % ( | ||||
|                 tostring(xml), tostring(sent_xml))) | ||||
|         if sent_data is None: | ||||
|             self.fail("No stanza was sent.") | ||||
|         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 stream_send_stanza(self, stanza_class, data, defaults=None, | ||||
|                            use_values=True, timeout=.1): | ||||
|     def send(self, data, defaults=None, use_values=True, | ||||
|              timeout=.5, method='exact'): | ||||
|         """ | ||||
|         Check that the XMPP client sent the given stanza XML. | ||||
|  | ||||
|         Extracts the next sent stanza and compares it with the given | ||||
|         XML using check_stanza. | ||||
|         XML using check. | ||||
|  | ||||
|         Arguments: | ||||
|             stanza_class -- The class of the sent stanza object. | ||||
| @@ -562,70 +604,26 @@ class SleekTest(unittest.TestCase): | ||||
|                             values which may interfere with comparisons. | ||||
|             timeout      -- Time in seconds to wait for a stanza before | ||||
|                             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 isintance(data, str): | ||||
|             data = stanza_class(xml=self.parse_xml(data)) | ||||
|         sent = self.xmpp.socket.next_sent(timeout) | ||||
|         self.check_stanza(stanza_class, data, sent, | ||||
|                           defaults=defaults, | ||||
|                           use_values=use_values) | ||||
|         if data is None and sent is None: | ||||
|             return | ||||
|         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.") | ||||
|  | ||||
|     def stream_send_message(self, data, use_values=True, timeout=.1): | ||||
|         """ | ||||
|         Check that the XMPP client sent the given stanza XML. | ||||
|  | ||||
|         Extracts the next sent stanza and compares it with the given | ||||
|         XML using check_message. | ||||
|  | ||||
|         Arguments: | ||||
|             data       -- The XML string of the expected Message stanza, | ||||
|                           or an equivalent stanza object. | ||||
|             use_values -- Modifies the type of tests used by check_message. | ||||
|             timeout    -- Time in seconds to wait for a stanza before | ||||
|                           failing the check. | ||||
|         """ | ||||
|         if isinstance(data, str): | ||||
|             data = self.Message(xml=self.parse_xml(data)) | ||||
|         sent = self.xmpp.socket.next_sent(timeout) | ||||
|         self.check_message(data, sent, use_values) | ||||
|  | ||||
|     def stream_send_iq(self, data, use_values=True, timeout=.1): | ||||
|         """ | ||||
|         Check that the XMPP client sent the given stanza XML. | ||||
|  | ||||
|         Extracts the next sent stanza and compares it with the given | ||||
|         XML using check_iq. | ||||
|  | ||||
|         Arguments: | ||||
|             data       -- The XML string of the expected Iq stanza, | ||||
|                           or an equivalent stanza object. | ||||
|             use_values -- Modifies the type of tests used by check_iq. | ||||
|             timeout    -- Time in seconds to wait for a stanza before | ||||
|                           failing the check. | ||||
|         """ | ||||
|         if isinstance(data, str): | ||||
|             data = self.Iq(xml=self.parse_xml(data)) | ||||
|         sent = self.xmpp.socket.next_sent(timeout) | ||||
|         self.check_iq(data, sent, use_values) | ||||
|  | ||||
|     def stream_send_presence(self, data, use_values=True, timeout=.1): | ||||
|         """ | ||||
|         Check that the XMPP client sent the given stanza XML. | ||||
|  | ||||
|         Extracts the next sent stanza and compares it with the given | ||||
|         XML using check_presence. | ||||
|  | ||||
|         Arguments: | ||||
|             data       -- The XML string of the expected Presence stanza, | ||||
|                           or an equivalent stanza object. | ||||
|             use_values -- Modifies the type of tests used by check_presence. | ||||
|             timeout    -- Time in seconds to wait for a stanza before | ||||
|                           failing the check. | ||||
|         """ | ||||
|         if isinstance(data, str): | ||||
|             data = self.Presence(xml=self.parse_xml(data)) | ||||
|         sent = self.xmpp.socket.next_sent(timeout) | ||||
|         self.check_presence(data, sent, use_values) | ||||
|         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): | ||||
|         """ | ||||
|   | ||||
							
								
								
									
										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 | ||||
							
								
								
									
										96
									
								
								sleekxmpp/thirdparty/statemachine.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										96
									
								
								sleekxmpp/thirdparty/statemachine.py
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ class StateMachine(object): | ||||
|         self.addStates(states) | ||||
|         self.__default_state = self.__states[0] | ||||
|         self.__current_state = self.__default_state | ||||
|      | ||||
|  | ||||
|     def addStates(self, states): | ||||
|         self.lock.acquire() | ||||
|         try: | ||||
| @@ -30,19 +30,19 @@ class StateMachine(object): | ||||
|                     raise IndexError("The state '%s' is already in the StateMachine." % state) | ||||
|                 self.__states.append(state) | ||||
|         finally: self.lock.release() | ||||
|      | ||||
|      | ||||
|  | ||||
|  | ||||
|     def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={}): | ||||
|         ''' | ||||
|         Transition from the given `from_state` to the given `to_state`.   | ||||
|         Transition from the given `from_state` to the given `to_state`. | ||||
|         This method will return `True` if the state machine is now in `to_state`.  It | ||||
|         will return `False` if a timeout occurred the transition did not occur.   | ||||
|         If `wait` is 0 (the default,) this method returns immediately if the state machine  | ||||
|         will return `False` if a timeout occurred the transition did not occur. | ||||
|         If `wait` is 0 (the default,) this method returns immediately if the state machine | ||||
|         is not in `from_state`. | ||||
|  | ||||
|         If you want the thread to block and transition once the state machine to enters | ||||
|         `from_state`, set `wait` to a non-negative value.  Note there is no 'block  | ||||
|         indefinitely' flag since this leads to deadlock.  If you want to wait indefinitely,  | ||||
|         `from_state`, set `wait` to a non-negative value.  Note there is no 'block | ||||
|         indefinitely' flag since this leads to deadlock.  If you want to wait indefinitely, | ||||
|         choose a reasonable value for `wait` (e.g. 20 seconds) and do so in a while loop like so: | ||||
|  | ||||
|         :: | ||||
| @@ -60,42 +60,42 @@ class StateMachine(object): | ||||
|         True value or if an exception is thrown, the transition will not occur.  Any thrown | ||||
|         exception is not caught by the state machine and is the caller's responsibility to handle. | ||||
|         If `func` completes normally, this method will return the value returned by `func.`  If | ||||
|         values for `args` and `kwargs` are provided, they are expanded and passed like so:   | ||||
|         values for `args` and `kwargs` are provided, they are expanded and passed like so: | ||||
|         `func( *args, **kwargs )`. | ||||
|         ''' | ||||
|  | ||||
|         return self.transition_any((from_state,), to_state, wait=wait,  | ||||
|         return self.transition_any((from_state,), to_state, wait=wait, | ||||
|                                     func=func, args=args, kwargs=kwargs) | ||||
|      | ||||
|      | ||||
|  | ||||
|  | ||||
|     def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={}): | ||||
|         ''' | ||||
|         Transition from any of the given `from_states` to the given `to_state`. | ||||
|         ''' | ||||
|  | ||||
|         if not (isinstance(from_states,tuple) or isinstance(from_states,list)):  | ||||
|         if not (isinstance(from_states,tuple) or isinstance(from_states,list)): | ||||
|                 raise ValueError("from_states should be a list or tuple") | ||||
|  | ||||
|         for state in from_states: | ||||
|             if not state in self.__states:  | ||||
|             if not state in self.__states: | ||||
|                 raise ValueError("StateMachine does not contain from_state %s." % state) | ||||
|         if not to_state in self.__states:  | ||||
|         if not to_state in self.__states: | ||||
|             raise ValueError("StateMachine does not contain to_state %s." % to_state) | ||||
|  | ||||
|         start = time.time() | ||||
|         while not self.lock.acquire(False): | ||||
|             time.sleep(.001) | ||||
|             if (start + wait - time.time()) <= 0.0: | ||||
|                 logging.debug("Could not acquire lock") | ||||
|                 log.debug("Could not acquire lock") | ||||
|                 return False | ||||
|  | ||||
|         while not self.__current_state in from_states: | ||||
|             # detect timeout: | ||||
|             remainder = start + wait - time.time() | ||||
|             if remainder > 0:  | ||||
|             if remainder > 0: | ||||
|                 self.notifier.wait(remainder) | ||||
|             else:  | ||||
|                 logging.debug("State was not ready") | ||||
|             else: | ||||
|                 log.debug("State was not ready") | ||||
|                 self.lock.release() | ||||
|                 return False | ||||
|  | ||||
| @@ -105,9 +105,9 @@ class StateMachine(object): | ||||
|                 # Note that func might throw an exception, but that's OK, it aborts the transition | ||||
|                 return_val = func(*args,**kwargs) if func is not None else True | ||||
|  | ||||
|                 # some 'false' value returned from func,  | ||||
|                 # some 'false' value returned from func, | ||||
|                 # indicating that transition should not occur: | ||||
|                 if not return_val: return return_val  | ||||
|                 if not return_val: return return_val | ||||
|  | ||||
|                 log.debug(' ==== TRANSITION %s -> %s', self.__current_state, to_state) | ||||
|                 self._set_state(to_state) | ||||
| @@ -115,7 +115,7 @@ class StateMachine(object): | ||||
|             else: | ||||
|                 log.error("StateMachine bug!!  The lock should ensure this doesn't happen!") | ||||
|                 return False | ||||
|         finally:  | ||||
|         finally: | ||||
|             self.notifier.set() # notify any waiting threads that the state has changed. | ||||
|             self.notifier.clear() | ||||
|             self.lock.release() | ||||
| @@ -125,13 +125,13 @@ class StateMachine(object): | ||||
|         ''' | ||||
|         Use the state machine as a context manager.  The transition occurs on /exit/ from | ||||
|         the `with` context, so long as no exception is thrown.  For example: | ||||
|          | ||||
|  | ||||
|         :: | ||||
|  | ||||
|             with state_machine.transition_ctx('one','two', wait=5) as locked: | ||||
|                 if locked: | ||||
|                     # the state machine is currently locked in state 'one', and will  | ||||
|                     # transition to 'two' when the 'with' statement ends, so long as  | ||||
|                     # the state machine is currently locked in state 'one', and will | ||||
|                     # transition to 'two' when the 'with' statement ends, so long as | ||||
|                     # no exception is thrown. | ||||
|                     print 'Currently locked in state one: %s' % state_machine['one'] | ||||
|  | ||||
| @@ -142,20 +142,20 @@ class StateMachine(object): | ||||
|             print 'Since no exception was thrown, we are now in state "two": %s' % state_machine['two'] | ||||
|  | ||||
|  | ||||
|         The other main difference between this method and `transition()` is that the  | ||||
|         state machine is locked for the duration of the `with` statement.  Normally,  | ||||
|         after a `transition()` occurs, the state machine is immediately unlocked and  | ||||
|         The other main difference between this method and `transition()` is that the | ||||
|         state machine is locked for the duration of the `with` statement.  Normally, | ||||
|         after a `transition()` occurs, the state machine is immediately unlocked and | ||||
|         available to another thread to call `transition()` again. | ||||
|         ''' | ||||
|  | ||||
|         if not from_state in self.__states:  | ||||
|         if not from_state in self.__states: | ||||
|             raise ValueError("StateMachine does not contain from_state %s." % from_state) | ||||
|         if not to_state in self.__states:  | ||||
|         if not to_state in self.__states: | ||||
|             raise ValueError("StateMachine does not contain to_state %s." % to_state) | ||||
|  | ||||
|         return _StateCtx(self, from_state, to_state, wait) | ||||
|  | ||||
|      | ||||
|  | ||||
|     def ensure(self, state, wait=0.0, block_on_transition=False): | ||||
|         ''' | ||||
|         Ensure the state machine is currently in `state`, or wait until it enters `state`. | ||||
| @@ -168,24 +168,24 @@ class StateMachine(object): | ||||
|         Ensure we are currently in one of the given `states` or wait until | ||||
|         we enter one of those states. | ||||
|  | ||||
|         Note that due to the nature of the function, you cannot guarantee that  | ||||
|         Note that due to the nature of the function, you cannot guarantee that | ||||
|         the entirety of some operation completes while you remain in a given | ||||
|         state.  That would require acquiring and holding a lock, which  | ||||
|         state.  That would require acquiring and holding a lock, which | ||||
|         would mean no other threads could do the same.  (You'd essentially | ||||
|         be serializing all of the threads that are 'ensuring' their tasks | ||||
|         occurred in some state.   | ||||
|         occurred in some state. | ||||
|         ''' | ||||
|         if not (isinstance(states,tuple) or isinstance(states,list)):  | ||||
|         if not (isinstance(states,tuple) or isinstance(states,list)): | ||||
|             raise ValueError('states arg should be a tuple or list') | ||||
|  | ||||
|         for state in states: | ||||
|             if not state in self.__states:  | ||||
|             if not state in self.__states: | ||||
|                 raise ValueError("StateMachine does not contain state '%s'" % state) | ||||
|  | ||||
|         # if we're in the middle of a transition, determine whether we should  | ||||
|         # 'fall back' to the 'current' state, or wait for the new state, in order to  | ||||
|         # if we're in the middle of a transition, determine whether we should | ||||
|         # 'fall back' to the 'current' state, or wait for the new state, in order to | ||||
|         # avoid an operation occurring in the wrong state. | ||||
|         # TODO another option would be an ensure_ctx that uses a semaphore to allow  | ||||
|         # TODO another option would be an ensure_ctx that uses a semaphore to allow | ||||
|         # threads to indicate they want to remain in a particular state. | ||||
|  | ||||
|         # will return immediately if no transition is in process. | ||||
| @@ -196,16 +196,16 @@ class StateMachine(object): | ||||
|             else: self.notifier.wait() | ||||
|  | ||||
|         start = time.time() | ||||
|         while not self.__current_state in states:  | ||||
|         while not self.__current_state in states: | ||||
|             # detect timeout: | ||||
|             remainder = start + wait - time.time() | ||||
|             if remainder > 0: self.notifier.wait(remainder) | ||||
|             else: return False | ||||
|         return True | ||||
|  | ||||
|      | ||||
|  | ||||
|     def reset(self): | ||||
|         # TODO need to lock before calling this?  | ||||
|         # TODO need to lock before calling this? | ||||
|         self.transition(self.__current_state, self.__default_state) | ||||
|  | ||||
|  | ||||
| @@ -231,7 +231,7 @@ class StateMachine(object): | ||||
|     def __str__(self): | ||||
|         return "".join(("StateMachine(", ','.join(self.__states), "): ", self.__current_state)) | ||||
|  | ||||
|      | ||||
|  | ||||
|  | ||||
| class _StateCtx: | ||||
|  | ||||
| @@ -244,28 +244,28 @@ class _StateCtx: | ||||
|  | ||||
|     def __enter__(self): | ||||
|         start = time.time() | ||||
|         while not self.state_machine[self.from_state] or not self.state_machine.lock.acquire(False):  | ||||
|         while not self.state_machine[self.from_state] or not self.state_machine.lock.acquire(False): | ||||
|             # detect timeout: | ||||
|             remainder = start + self.wait - time.time() | ||||
|             if remainder > 0: self.state_machine.notifier.wait(remainder) | ||||
|             else:  | ||||
|             else: | ||||
|                 log.debug('StateMachine timeout while waiting for state: %s', self.from_state) | ||||
|                 return False | ||||
|  | ||||
|         self._locked = True # lock has been acquired at this point | ||||
|         self.state_machine.notifier.clear() | ||||
|         log.debug('StateMachine entered context in state: %s',  | ||||
|         log.debug('StateMachine entered context in state: %s', | ||||
|                 self.state_machine.current_state()) | ||||
|         return True | ||||
|  | ||||
|     def __exit__(self, exc_type, exc_val, exc_tb): | ||||
|         if exc_val is not None: | ||||
|             log.exception("StateMachine exception in context, remaining in state: %s\n%s:%s",  | ||||
|             log.exception("StateMachine exception in context, remaining in state: %s\n%s:%s", | ||||
|                 self.state_machine.current_state(), exc_type.__name__, exc_val) | ||||
|  | ||||
|         if self._locked: | ||||
|             if exc_val is None: | ||||
|                 log.debug(' ==== TRANSITION %s -> %s',  | ||||
|                 log.debug(' ==== TRANSITION %s -> %s', | ||||
|                         self.state_machine.current_state(), self.to_state) | ||||
|                 self.state_machine._set_state(self.to_state) | ||||
|  | ||||
|   | ||||
| @@ -22,6 +22,8 @@ class FileSocket(_fileobject): | ||||
|  | ||||
|     def read(self, size=4096): | ||||
|         """Read data from the socket as if it were a file.""" | ||||
|         if self._sock is None: | ||||
|             return None | ||||
|         data = self._sock.recv(size) | ||||
|         if data is not None: | ||||
|             return data | ||||
|   | ||||
| @@ -42,8 +42,6 @@ class BaseHandler(object): | ||||
|                        this handler. | ||||
|             stream  -- The XMLStream instance the handler should monitor. | ||||
|         """ | ||||
|         self.checkDelete = self.check_delete | ||||
|  | ||||
|         self.name = name | ||||
|         self.stream = stream | ||||
|         self._destroy = False | ||||
| @@ -87,3 +85,8 @@ class BaseHandler(object): | ||||
|         handlers. | ||||
|         """ | ||||
|         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: | ||||
|             payload -- The matched stanza object. | ||||
|         """ | ||||
|         BaseHandler.prerun(self, payload) | ||||
|         if self._once: | ||||
|             self._destroy = True | ||||
|         if self._instream: | ||||
|             self.run(payload, True) | ||||
|  | ||||
| @@ -78,7 +79,7 @@ class Callback(BaseHandler): | ||||
|                         Defaults to False. | ||||
|         """ | ||||
|         if not self._instream or instream: | ||||
|             BaseHandler.run(self, payload) | ||||
|             self._pointer(payload) | ||||
|             if self._once: | ||||
|                 self._destroy = True | ||||
|                 del self._pointer | ||||
|   | ||||
| @@ -12,10 +12,13 @@ try: | ||||
| except ImportError: | ||||
|     import Queue as queue | ||||
|  | ||||
| from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT | ||||
| from sleekxmpp.xmlstream import StanzaBase | ||||
| from sleekxmpp.xmlstream.handler.base import BaseHandler | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class Waiter(BaseHandler): | ||||
|  | ||||
|     """ | ||||
| @@ -66,7 +69,7 @@ class Waiter(BaseHandler): | ||||
|         """ | ||||
|         pass | ||||
|  | ||||
|     def wait(self, timeout=RESPONSE_TIMEOUT): | ||||
|     def wait(self, timeout=None): | ||||
|         """ | ||||
|         Block an event handler while waiting for a stanza to arrive. | ||||
|  | ||||
| @@ -81,11 +84,14 @@ class Waiter(BaseHandler): | ||||
|                        arrive. Defaults to the global default timeout | ||||
|                        value sleekxmpp.xmlstream.RESPONSE_TIMEOUT. | ||||
|         """ | ||||
|         if timeout is None: | ||||
|             timeout = self.stream.response_timeout | ||||
|  | ||||
|         try: | ||||
|             stanza = self._payload.get(True, timeout) | ||||
|         except queue.Empty: | ||||
|             stanza = False | ||||
|             logging.warning("Timed out waiting for %s" % self.name) | ||||
|             log.warning("Timed out waiting for %s" % self.name) | ||||
|         self.stream.removeHandler(self.name) | ||||
|         return stanza | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,8 @@ | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
|  | ||||
| class JID(object): | ||||
|     """ | ||||
| @@ -42,7 +44,9 @@ class JID(object): | ||||
|         Arguments: | ||||
|             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._resource = None | ||||
|         self._user = None | ||||
| @@ -57,7 +61,7 @@ class JID(object): | ||||
|                     full, or bare. | ||||
|         """ | ||||
|         if name == 'resource': | ||||
|             if self._resource is None: | ||||
|             if self._resource is None and '/' in self._jid: | ||||
|                 self._resource = self._jid.split('/', 1)[-1] | ||||
|             return self._resource or "" | ||||
|         elif name == 'user': | ||||
| @@ -71,7 +75,7 @@ class JID(object): | ||||
|             if self._domain is None: | ||||
|                 self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0] | ||||
|             return self._domain or "" | ||||
|         elif name == 'full': | ||||
|         elif name in ('full', 'jid'): | ||||
|             return self._jid or "" | ||||
|         elif name == 'bare': | ||||
|             if self._bare is None: | ||||
| @@ -94,21 +98,15 @@ class JID(object): | ||||
|         elif name in ('server', 'domain', 'host'): | ||||
|             self.domain = value | ||||
|         elif name in ('full', 'jid'): | ||||
|             if '@' not in value: | ||||
|                 if '/' in value: | ||||
|                     d, r = value.split('/', 1) | ||||
|                     object.__setattr__(self, "_resource", r) | ||||
|                 else: | ||||
|                     d = value | ||||
|                 object.__setattr__(self, "_domain", d) | ||||
|             else: | ||||
|                 self.reset(value) | ||||
|             self.reset(value) | ||||
|             self.regenerate() | ||||
|         elif name == 'bare': | ||||
|             if '@' in value: | ||||
|                 u, d = value.split('@', 1) | ||||
|                 object.__setattr__(self, "_user", u) | ||||
|                 object.__setattr__(self, "_domain", d) | ||||
|             else: | ||||
|                 object.__setattr__(self, "_user", '') | ||||
|                 object.__setattr__(self, "_domain", value) | ||||
|             self.regenerate() | ||||
|         else: | ||||
| @@ -127,3 +125,13 @@ class JID(object): | ||||
|     def __str__(self): | ||||
|         """Use the full JID as the string value.""" | ||||
|         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 | ||||
|   | ||||
| @@ -6,6 +6,8 @@ | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| import logging | ||||
|  | ||||
| from xml.parsers.expat import ExpatError | ||||
|  | ||||
| from sleekxmpp.xmlstream.stanzabase import ET | ||||
| @@ -18,6 +20,9 @@ from sleekxmpp.xmlstream.matcher.base import MatcherBase | ||||
| IGNORE_NS = False | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class MatchXMLMask(MatcherBase): | ||||
|  | ||||
|     """ | ||||
| @@ -97,8 +102,7 @@ class MatchXMLMask(MatcherBase): | ||||
|             try: | ||||
|                 mask = ET.fromstring(mask) | ||||
|             except ExpatError: | ||||
|                 logging.log(logging.WARNING, | ||||
|                             "Expat error: %s\nIn parsing: %s" % ('', mask)) | ||||
|                 log.warning("Expat error: %s\nIn parsing: %s" % ('', mask)) | ||||
|  | ||||
|         if not use_ns: | ||||
|             # Compare the element without using namespaces. | ||||
| @@ -113,7 +117,8 @@ class MatchXMLMask(MatcherBase): | ||||
|                 return False | ||||
|  | ||||
|         # 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 | ||||
|  | ||||
|         # Compare attributes. The stanza must include the attributes | ||||
| @@ -123,10 +128,17 @@ class MatchXMLMask(MatcherBase): | ||||
|                 return False | ||||
|  | ||||
|         # Recursively check subelements. | ||||
|         matched_elements = {} | ||||
|         for subelement in mask: | ||||
|             if use_ns: | ||||
|                 if not self._mask_cmp(source.find(subelement.tag), | ||||
|                                       subelement, use_ns): | ||||
|                 matched = False | ||||
|                 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 | ||||
|             else: | ||||
|                 if not self._mask_cmp(self._get_child(source, subelement.tag), | ||||
|   | ||||
| @@ -15,6 +15,9 @@ except ImportError: | ||||
|     import Queue as queue | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class Task(object): | ||||
|  | ||||
|     """ | ||||
| @@ -129,6 +132,7 @@ class Scheduler(object): | ||||
|         if threaded: | ||||
|             self.thread = threading.Thread(name='sheduler_process', | ||||
|                                            target=self._process) | ||||
|             self.thread.daemon = True | ||||
|             self.thread.start() | ||||
|         else: | ||||
|             self._process() | ||||
| @@ -137,7 +141,8 @@ class Scheduler(object): | ||||
|         """Process scheduled tasks.""" | ||||
|         self.run = True | ||||
|         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 | ||||
|                     updated = False | ||||
|                     if self.schedule: | ||||
| @@ -146,6 +151,8 @@ class Scheduler(object): | ||||
|                         if wait <= 0.0: | ||||
|                             newtask = self.addq.get(False) | ||||
|                         else: | ||||
|                             if wait >= 3.0: | ||||
|                                 wait = 3.0 | ||||
|                             newtask = self.addq.get(True, wait) | ||||
|                     except queue.Empty: | ||||
|                         cleanup = [] | ||||
| @@ -168,13 +175,13 @@ class Scheduler(object): | ||||
|         except KeyboardInterrupt: | ||||
|             self.run = False | ||||
|             if self.parentstop is not None: | ||||
|                 logging.debug("stopping parent") | ||||
|                 log.debug("stopping parent") | ||||
|                 self.parentstop.set() | ||||
|         except SystemExit: | ||||
|             self.run = False | ||||
|             if self.parentstop is not None: | ||||
|                 self.parentstop.set() | ||||
|         logging.debug("Quitting Scheduler thread") | ||||
|         log.debug("Quitting Scheduler thread") | ||||
|         if self.parentqueue is not None: | ||||
|             self.parentqueue.put(('quit', None, None)) | ||||
|  | ||||
|   | ||||
| @@ -14,23 +14,42 @@ from xml.etree import cElementTree as ET | ||||
|  | ||||
| from sleekxmpp.xmlstream import JID | ||||
| from sleekxmpp.xmlstream.tostring import tostring | ||||
| from sleekxmpp.thirdparty import OrderedDict | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| # Used to check if an argument is an XML object. | ||||
| 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. | ||||
|  | ||||
|     Arguments: | ||||
|         stanza -- The class of the parent stanza. | ||||
|         plugin -- The class of the plugin stanza. | ||||
|         stanza    -- The class of the parent 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) | ||||
|     stanza.plugin_attrib_map[plugin.plugin_attrib] = 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. | ||||
| @@ -92,10 +111,22 @@ class ElementBase(object): | ||||
|     >>> message['custom']['useful_thing'] = 'foo' | ||||
|  | ||||
|     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 | ||||
|     directly from the parent stanza, as so: | ||||
|     plugin_attrib value, then the plugin's interface may be assigned | ||||
|     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']['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: | ||||
|         name              -- The name of the stanza's main element. | ||||
| @@ -105,14 +136,35 @@ class ElementBase(object): | ||||
|         sub_interfaces    -- A subset of the set of interfaces which map | ||||
|                              to subelements instead of attributes. | ||||
|         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. | ||||
|         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 | ||||
|                              accessed as a plugin from another stanza. | ||||
|         plugin_attrib_map -- A mapping of plugin attribute names with the | ||||
|                              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 | ||||
|                              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: | ||||
|         xml               -- The stanza's XML contents. | ||||
| @@ -122,6 +174,10 @@ class ElementBase(object): | ||||
|         values            -- A dictionary of the stanza's interfaces | ||||
|                              and interface values, including plugins. | ||||
|  | ||||
|     Class Methods | ||||
|         tag_name -- Return the namespaced version of the stanza's | ||||
|                     root element's name. | ||||
|  | ||||
|     Methods: | ||||
|         setup              -- Initialize the stanza's XML contents. | ||||
|         enable             -- Instantiate a stanza plugin. | ||||
| @@ -141,7 +197,7 @@ class ElementBase(object): | ||||
|         _get_attr          -- Return an attribute's value from the main | ||||
|                               stanza element. | ||||
|         _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. | ||||
|         match              -- Compare the stanza against an XPath expression. | ||||
|         find               -- Return subelement matching an XPath expression. | ||||
| @@ -154,6 +210,7 @@ class ElementBase(object): | ||||
|         appendxml          -- Add XML content to the stanza. | ||||
|         pop                -- Remove a substanza. | ||||
|         next               -- Return the next iterable substanza. | ||||
|         clear              -- Reset the stanza's XML contents. | ||||
|         _fix_ns            -- Apply the stanza's namespace to non-namespaced | ||||
|                               elements in an XPath expression. | ||||
|     """ | ||||
| @@ -164,9 +221,14 @@ class ElementBase(object): | ||||
|     interfaces = set(('type', 'to', 'from', 'id', 'payload')) | ||||
|     types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) | ||||
|     sub_interfaces = tuple() | ||||
|     overrides = {} | ||||
|     plugin_attrib_map = {} | ||||
|     plugin_overrides = {} | ||||
|     plugin_iterables = set() | ||||
|     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): | ||||
|         """ | ||||
| @@ -176,22 +238,11 @@ class ElementBase(object): | ||||
|             xml    -- Initialize the stanza with optional existing XML. | ||||
|             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.plugins = {} | ||||
|         self.plugins = OrderedDict() | ||||
|         self.iterables = [] | ||||
|         self._index = 0 | ||||
|         self.tag = self.tag_name() | ||||
|         if parent is None: | ||||
|             self.parent = None | ||||
|         else: | ||||
| @@ -200,6 +251,10 @@ class ElementBase(object): | ||||
|         ElementBase.values = property(ElementBase._get_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 we generated our own XML, then everything is ready. | ||||
|             return | ||||
| @@ -209,11 +264,10 @@ class ElementBase(object): | ||||
|             if child.tag in self.plugin_tag_map: | ||||
|                 plugin = self.plugin_tag_map[child.tag] | ||||
|                 self.plugins[plugin.plugin_attrib] = plugin(child, self) | ||||
|             if self.subitem is not None: | ||||
|                 for sub in self.subitem: | ||||
|                     if child.tag == "{%s}%s" % (sub.namespace, sub.name): | ||||
|                         self.iterables.append(sub(child, self)) | ||||
|                         break | ||||
|             for sub in self.plugin_iterables: | ||||
|                 if child.tag == "{%s}%s" % (sub.namespace, sub.name): | ||||
|                     self.iterables.append(sub(child, self)) | ||||
|                     break | ||||
|  | ||||
|     def setup(self, xml=None): | ||||
|         """ | ||||
| @@ -280,14 +334,12 @@ class ElementBase(object): | ||||
|         for interface in self.interfaces: | ||||
|             values[interface] = self[interface] | ||||
|         for plugin, stanza in self.plugins.items(): | ||||
|             values[plugin] = stanza._get_stanza_values() | ||||
|             values[plugin] = stanza.values | ||||
|         if self.iterables: | ||||
|             iterables = [] | ||||
|             for stanza in self.iterables: | ||||
|                 iterables.append(stanza._get_stanza_values()) | ||||
|                 iterables[-1].update({ | ||||
|                     '__childtag__': "{%s}%s" % (stanza.namespace, | ||||
|                                                 stanza.name)}) | ||||
|                 iterables.append(stanza.values) | ||||
|                 iterables[-1]['__childtag__'] = stanza.tag | ||||
|             values['substanzas'] = iterables | ||||
|         return values | ||||
|  | ||||
| @@ -302,24 +354,34 @@ class ElementBase(object): | ||||
|                       Plugin interfaces may accept a nested dictionary that | ||||
|                       will be used recursively. | ||||
|         """ | ||||
|         iterable_interfaces = [p.plugin_attrib for \ | ||||
|                                     p in self.plugin_iterables] | ||||
|  | ||||
|         for interface, value in values.items(): | ||||
|             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: | ||||
|                     if '__childtag__' in subdict: | ||||
|                         for subclass in self.subitem: | ||||
|                         for subclass in self.plugin_iterables: | ||||
|                             child_tag = "{%s}%s" % (subclass.namespace, | ||||
|                                                     subclass.name) | ||||
|                             if subdict['__childtag__'] == child_tag: | ||||
|                                 sub = subclass(parent=self) | ||||
|                                 sub._set_stanza_values(subdict) | ||||
|                                 sub.values = subdict | ||||
|                                 self.iterables.append(sub) | ||||
|                                 break | ||||
|             elif interface in self.interfaces: | ||||
|                 self[interface] = value | ||||
|             elif interface in self.plugin_attrib_map: | ||||
|                 if interface not in self.plugins: | ||||
|                     self.init_plugin(interface) | ||||
|                 self.plugins[interface]._set_stanza_values(value) | ||||
|                 if interface not in iterable_interfaces: | ||||
|                     if interface not in self.plugins: | ||||
|                         self.init_plugin(interface) | ||||
|                     self.plugins[interface].values = value | ||||
|         return self | ||||
|  | ||||
|     def __getitem__(self, attrib): | ||||
| @@ -337,12 +399,13 @@ class ElementBase(object): | ||||
|         The search order for interface value retrieval for an interface | ||||
|         named 'foo' is: | ||||
|             1. The list of substanzas. | ||||
|             2. The result of calling get_foo. | ||||
|             3. The result of calling getFoo. | ||||
|             4. The contents of the foo subelement, if foo is a sub interface. | ||||
|             5. The value of the foo attribute of the XML object. | ||||
|             6. The plugin named 'foo' | ||||
|             7. An empty string. | ||||
|             2. The result of calling the get_foo override handler. | ||||
|             3. The result of calling get_foo. | ||||
|             4. The result of calling getFoo. | ||||
|             5. The contents of the foo subelement, if foo is a sub interface. | ||||
|             6. The value of the foo attribute of the XML object. | ||||
|             7. The plugin named 'foo' | ||||
|             8. An empty string. | ||||
|  | ||||
|         Arguments: | ||||
|             attrib -- The name of the requested stanza interface. | ||||
| @@ -352,6 +415,16 @@ class ElementBase(object): | ||||
|         elif attrib in self.interfaces: | ||||
|             get_method = "get_%s" % attrib.lower() | ||||
|             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): | ||||
|                 return getattr(self, get_method)() | ||||
|             elif hasattr(self, get_method2): | ||||
| @@ -364,6 +437,8 @@ class ElementBase(object): | ||||
|         elif attrib in self.plugin_attrib_map: | ||||
|             if attrib not in self.plugins: | ||||
|                 self.init_plugin(attrib) | ||||
|             if self.plugins[attrib].is_extension: | ||||
|                 return self.plugins[attrib][attrib] | ||||
|             return self.plugins[attrib] | ||||
|         else: | ||||
|             return '' | ||||
| @@ -384,13 +459,14 @@ class ElementBase(object): | ||||
|         The effect of interface value assignment for an interface | ||||
|         named 'foo' will be one of: | ||||
|             1. Delete the interface's contents if the value is None. | ||||
|             2. Call set_foo, if it exists. | ||||
|             3. Call setFoo, if it exists. | ||||
|             4. Set the text of a foo element, if foo is in sub_interfaces. | ||||
|             5. Set the value of a top level XML attribute name foo. | ||||
|             6. Attempt to pass value to a plugin named foo using the plugin's | ||||
|             2. Call the set_foo override handler, if it exists. | ||||
|             3. Call set_foo, if it exists. | ||||
|             4. Call setFoo, if it exists. | ||||
|             5. Set the text of a foo element, if foo is in sub_interfaces. | ||||
|             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. | ||||
|             7. Do nothing. | ||||
|             8. Do nothing. | ||||
|  | ||||
|         Arguments: | ||||
|             attrib -- The name of the stanza interface to modify. | ||||
| @@ -400,6 +476,16 @@ class ElementBase(object): | ||||
|             if value is not None: | ||||
|                 set_method = "set_%s" % attrib.lower() | ||||
|                 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): | ||||
|                     getattr(self, set_method)(value,) | ||||
|                 elif hasattr(self, set_method2): | ||||
| @@ -435,12 +521,13 @@ class ElementBase(object): | ||||
|  | ||||
|         The effect of deleting a stanza interface value named foo will be | ||||
|         one of: | ||||
|             1. Call del_foo, if it exists. | ||||
|             2. Call delFoo, if it exists. | ||||
|             3. Delete foo element, if foo is in sub_interfaces. | ||||
|             4. Delete top level XML attribute named foo. | ||||
|             5. Remove the foo plugin, if it was loaded. | ||||
|             6. Do nothing. | ||||
|             1. Call del_foo override handler, if it exists. | ||||
|             2. Call del_foo, if it exists. | ||||
|             3. Call delFoo, if it exists. | ||||
|             4. Delete foo element, if foo is in sub_interfaces. | ||||
|             5. Delete top level XML attribute named foo. | ||||
|             6. Remove the foo plugin, if it was loaded. | ||||
|             7. Do nothing. | ||||
|  | ||||
|         Arguments: | ||||
|             attrib -- The name of the affected stanza interface. | ||||
| @@ -448,6 +535,16 @@ class ElementBase(object): | ||||
|         if attrib in self.interfaces: | ||||
|             del_method = "del_%s" % attrib.lower() | ||||
|             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): | ||||
|                 getattr(self, del_method)() | ||||
|             elif hasattr(self, del_method2): | ||||
| @@ -460,8 +557,13 @@ class ElementBase(object): | ||||
|         elif attrib in self.plugin_attrib_map: | ||||
|             if attrib in self.plugins: | ||||
|                 xml = self.plugins[attrib].xml | ||||
|                 if self.plugins[attrib].is_extension: | ||||
|                     del self.plugins[attrib][attrib] | ||||
|                 del self.plugins[attrib] | ||||
|                 self.xml.remove(xml) | ||||
|                 try: | ||||
|                     self.xml.remove(xml) | ||||
|                 except: | ||||
|                     pass | ||||
|         return self | ||||
|  | ||||
|     def _set_attr(self, name, value): | ||||
| @@ -783,6 +885,28 @@ class ElementBase(object): | ||||
|         """ | ||||
|         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 | ||||
|     def attrib(self): | ||||
|         """ | ||||
| @@ -855,13 +979,13 @@ class ElementBase(object): | ||||
|             return False | ||||
|  | ||||
|         # Check that this stanza is a superset of the other stanza. | ||||
|         values = self._get_stanza_values() | ||||
|         values = self.values | ||||
|         for key in other.keys(): | ||||
|             if key not in values or values[key] != other[key]: | ||||
|                 return False | ||||
|  | ||||
|         # Check that the other stanza is a superset of this stanza. | ||||
|         values = other._get_stanza_values() | ||||
|         values = other.values | ||||
|         for key in self.keys(): | ||||
|             if key not in values or values[key] != self[key]: | ||||
|                 return False | ||||
| @@ -931,11 +1055,16 @@ class ElementBase(object): | ||||
|         """ | ||||
|         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. | ||||
|  | ||||
|         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): | ||||
|         """ | ||||
| @@ -965,7 +1094,6 @@ class StanzaBase(ElementBase): | ||||
|  | ||||
|     Attributes: | ||||
|         stream -- The XMLStream instance that will handle sending this stanza. | ||||
|         tag    -- The namespaced version of the stanza's name. | ||||
|  | ||||
|     Methods: | ||||
|         set_type    -- Set the type of the stanza. | ||||
| @@ -976,7 +1104,6 @@ class StanzaBase(ElementBase): | ||||
|         get_payload -- Return the stanza's XML contents. | ||||
|         set_payload -- Append to 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' | ||||
|                        attributes to prepare for sending a reply. | ||||
|         error       -- Set the stanza's type to 'error'. | ||||
| @@ -1006,17 +1133,6 @@ class StanzaBase(ElementBase): | ||||
|             sfrom  -- Optional string or JID object of the sender's JID. | ||||
|             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 | ||||
|         if stream is not None: | ||||
|             self.namespace = stream.default_ns | ||||
| @@ -1091,24 +1207,17 @@ class StanzaBase(ElementBase): | ||||
|         self.clear() | ||||
|         return self | ||||
|  | ||||
|     def clear(self): | ||||
|     def reply(self, clear=True): | ||||
|         """ | ||||
|         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 | ||||
|  | ||||
|     def reply(self): | ||||
|         """ | ||||
|         Reset the stanza and swap its 'from' and 'to' attributes to prepare | ||||
|         for sending a reply stanza. | ||||
|         Swap the 'from' and 'to' attributes to prepare the stanza for | ||||
|         sending a reply. If clear=True, then also remove the stanza's | ||||
|         contents to make room for the reply content. | ||||
|  | ||||
|         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 self.stream and hasattr(self.stream, "is_component") and \ | ||||
| @@ -1117,7 +1226,8 @@ class StanzaBase(ElementBase): | ||||
|         else: | ||||
|             self['to'] = self['from'] | ||||
|             del self['from'] | ||||
|         self.clear() | ||||
|         if clear: | ||||
|             self.clear() | ||||
|         return self | ||||
|  | ||||
|     def error(self): | ||||
| @@ -1140,12 +1250,18 @@ class StanzaBase(ElementBase): | ||||
|  | ||||
|         Meant to be overridden. | ||||
|         """ | ||||
|         logging.exception('Error handling {%s}%s stanza' % (self.namespace, | ||||
|         log.exception('Error handling {%s}%s stanza' % (self.namespace, | ||||
|                                                             self.name)) | ||||
|  | ||||
|     def send(self): | ||||
|         """Queue the stanza to be sent on the XML stream.""" | ||||
|         self.stream.sendRaw(self.__str__()) | ||||
|     def send(self, now=False): | ||||
|         """ | ||||
|         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): | ||||
|         """ | ||||
| @@ -1155,8 +1271,37 @@ class StanzaBase(ElementBase): | ||||
|         return self.__class__(xml=copy.deepcopy(self.xml), | ||||
|                               stream=self.stream) | ||||
|  | ||||
|     def __str__(self): | ||||
|         """Serialize the stanza's XML to a string.""" | ||||
|     def __str__(self, top_level_ns=False): | ||||
|         """ | ||||
|         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='', | ||||
|                         stanza_ns=self.namespace, | ||||
|                         stanza_ns=stanza_ns, | ||||
|                         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. | ||||
|     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)) | ||||
|         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 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. | ||||
|     for attrib, value in xml.attrib.items(): | ||||
|         if '{' not in attrib: | ||||
|             value = xml_escape(value) | ||||
|             output.append(u' %s="%s"' % (attrib, value)) | ||||
|         value = xml_escape(value) | ||||
|         if '}' not in attrib: | ||||
|             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 there are additional child elements to serialize. | ||||
| @@ -68,9 +77,6 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): | ||||
|             for child in xml.getchildren(): | ||||
|                 output.append(tostring(child, tag_xmlns, stanza_ns, stream)) | ||||
|         output.append(u"</%s>" % tag_name) | ||||
|         if xml.tail: | ||||
|             # If there is additional text after the element. | ||||
|             output.append(xml_escape(xml.tail)) | ||||
|     elif xml.text: | ||||
|         # If we only have text content. | ||||
|         output.append(u">%s</%s>" % (xml_escape(xml.text), tag_name)) | ||||
|   | ||||
| @@ -10,13 +10,14 @@ from __future__ import with_statement, unicode_literals | ||||
|  | ||||
| import copy | ||||
| import logging | ||||
| import signal | ||||
| import socket as Socket | ||||
| import ssl | ||||
| import sys | ||||
| import threading | ||||
| import time | ||||
| import types | ||||
| import signal | ||||
| import random | ||||
| try: | ||||
|     import queue | ||||
| except ImportError: | ||||
| @@ -25,6 +26,8 @@ except ImportError: | ||||
| from sleekxmpp.thirdparty.statemachine import StateMachine | ||||
| from sleekxmpp.xmlstream import Scheduler, tostring | ||||
| 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 | ||||
| # wrapper is provided for this case in filesocket.py. | ||||
| @@ -43,6 +46,12 @@ HANDLER_THREADS = 1 | ||||
| # Flag indicating if the SSL library is available for use. | ||||
| SSL_SUPPORT = True | ||||
|  | ||||
| # Maximum time to delay between connection attempts is one hour. | ||||
| RECONNECT_MAX_DELAY = 3600 | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class RestartStream(Exception): | ||||
|     """ | ||||
| @@ -87,6 +96,10 @@ class XMLStream(object): | ||||
|         send_queue    -- A queue of stanzas to be sent on the stream. | ||||
|         socket        -- The connection to the server. | ||||
|         ssl_support   -- Indicates if a SSL library is available for use. | ||||
|         ssl_version   -- The version of the SSL protocol to use. | ||||
|                          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 | ||||
|                          connection state. | ||||
|         stream_footer -- The start tag and any attributes for the stream's | ||||
| @@ -95,7 +108,11 @@ class XMLStream(object): | ||||
|         use_ssl       -- Flag indicating if SSL should be used. | ||||
|         use_tls       -- Flag indicating if TLS should be used. | ||||
|         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: | ||||
|         add_event_handler    -- Add a handler for a custom event. | ||||
| @@ -141,20 +158,13 @@ class XMLStream(object): | ||||
|             port   -- The port to use for the connection. | ||||
|                       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_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._set_state('disconnected') | ||||
| @@ -178,11 +188,14 @@ class XMLStream(object): | ||||
|         self.stop = threading.Event() | ||||
|         self.stream_end_event = threading.Event() | ||||
|         self.stream_end_event.set() | ||||
|         self.session_started_event = threading.Event() | ||||
|  | ||||
|         self.event_queue = queue.Queue() | ||||
|         self.send_queue = queue.Queue() | ||||
|         self.__failed_send_stanza = None | ||||
|         self.scheduler = Scheduler(self.event_queue, self.stop) | ||||
|  | ||||
|         self.namespace_map = {} | ||||
|         self.namespace_map = {StanzaBase.xml_ns: 'xml'} | ||||
|  | ||||
|         self.__thread = {} | ||||
|         self.__root_stanza = [] | ||||
| @@ -196,16 +209,52 @@ class XMLStream(object): | ||||
|         self.auto_reconnect = True | ||||
|         self.is_client = False | ||||
|  | ||||
|         signal.signal(signal.SIGHUP, self._handle_kill) | ||||
|         signal.signal(signal.SIGTERM, self._handle_kill) # used in Windows | ||||
|     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. | ||||
|  | ||||
|     def _handle_kill(self, signum, frame): | ||||
|         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']. | ||||
|         """ | ||||
|         Capture kill event and disconnect cleanly after first | ||||
|         spawning the "killed" event. | ||||
|         """ | ||||
|         self.event("killed", direct=True) | ||||
|         self.disconnect() | ||||
|         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: | ||||
|             for sig_name in signals: | ||||
|                 if hasattr(signal, sig_name): | ||||
|                     sig = getattr(signal, sig_name) | ||||
|                     signal.signal(sig, handle_kill) | ||||
|             self.__signals_installed = True | ||||
|         except: | ||||
|             log.debug("Can not set interrupt signal handlers. " + \ | ||||
|                       "SleekXMPP is not running from a main thread.") | ||||
|  | ||||
|     def new_id(self): | ||||
|         """ | ||||
| @@ -264,9 +313,26 @@ class XMLStream(object): | ||||
|         self.stop.clear() | ||||
|         self.socket = self.socket_class(Socket.AF_INET, Socket.SOCK_STREAM) | ||||
|         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: | ||||
|             logging.debug("Socket Wrapped for SSL") | ||||
|             ssl_socket = ssl.wrap_socket(self.socket) | ||||
|             log.debug("Socket Wrapped for SSL") | ||||
|             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'): | ||||
|                 # We are using a testing socket, so preserve the top | ||||
|                 # layer of wrapping. | ||||
| @@ -275,17 +341,19 @@ class XMLStream(object): | ||||
|                 self.socket = ssl_socket | ||||
|  | ||||
|         try: | ||||
|             logging.debug("Connecting to %s:%s" % self.address) | ||||
|             log.debug("Connecting to %s:%s" % self.address) | ||||
|             self.socket.connect(self.address) | ||||
|             self.set_socket(self.socket, ignore=True) | ||||
|             #this event is where you should set your application state | ||||
|             self.event("connected", direct=True) | ||||
|             self.reconnect_delay = 1.0 | ||||
|             return True | ||||
|         except Socket.error as serr: | ||||
|             error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" | ||||
|             logging.error(error_msg % (self.address[0], self.address[1], | ||||
|             self.event('socket_error', serr) | ||||
|             log.error(error_msg % (self.address[0], self.address[1], | ||||
|                                        serr.errno, serr.strerror)) | ||||
|             time.sleep(1) | ||||
|             self.reconnect_delay = delay | ||||
|             return False | ||||
|  | ||||
|     def disconnect(self, reconnect=False): | ||||
| @@ -305,11 +373,11 @@ class XMLStream(object): | ||||
|  | ||||
|     def _disconnect(self, reconnect=False): | ||||
|         # 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 | ||||
|         # closed in the other direction. | ||||
|         if not reconnect: | ||||
|             self.auto_reconnect = False | ||||
|         self.auto_reconnect = reconnect | ||||
|         self.stream_end_event.wait(4) | ||||
|         if not self.auto_reconnect: | ||||
|             self.stop.set() | ||||
| @@ -318,9 +386,10 @@ class XMLStream(object): | ||||
|             self.filesocket.close() | ||||
|             self.socket.shutdown(Socket.SHUT_RDWR) | ||||
|         except Socket.error as serr: | ||||
|             pass | ||||
|             self.event('socket_error', serr) | ||||
|         finally: | ||||
|             #clear your application state | ||||
|             self.event('session_end', direct=True) | ||||
|             self.event("disconnected", direct=True) | ||||
|             return True | ||||
|  | ||||
| @@ -328,10 +397,10 @@ class XMLStream(object): | ||||
|         """ | ||||
|         Reset the stream's state and reconnect to the server. | ||||
|         """ | ||||
|         logging.debug("reconnecting...") | ||||
|         log.debug("reconnecting...") | ||||
|         self.state.transition('connected', 'disconnected', wait=2.0, | ||||
|                               func=self._disconnect, args=(True,)) | ||||
|         logging.debug("connecting...") | ||||
|         log.debug("connecting...") | ||||
|         return self.state.transition('disconnected', 'connected', | ||||
|                                      wait=2.0, func=self._connect) | ||||
|  | ||||
| @@ -368,10 +437,19 @@ class XMLStream(object): | ||||
|         to be restarted. | ||||
|         """ | ||||
|         if self.ssl_support: | ||||
|             logging.info("Negotiating TLS") | ||||
|             log.info("Negotiating TLS") | ||||
|             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_version=ssl.PROTOCOL_TLSv1, | ||||
|                                          do_handshake_on_connect=False) | ||||
|                                          ssl_version=self.ssl_version, | ||||
|                                          do_handshake_on_connect=False, | ||||
|                                          ca_certs=self.ca_certs, | ||||
|                                          cert_reqs=cert_policy) | ||||
|  | ||||
|             if hasattr(self.socket, 'socket'): | ||||
|                 # We are using a testing socket, so preserve the top | ||||
|                 # layer of wrapping. | ||||
| @@ -382,7 +460,7 @@ class XMLStream(object): | ||||
|             self.set_socket(self.socket) | ||||
|             return True | ||||
|         else: | ||||
|             logging.warning("Tried to enable TLS, but ssl module not found.") | ||||
|             log.warning("Tried to enable TLS, but ssl module not found.") | ||||
|             return False | ||||
|  | ||||
|     def start_stream_handler(self, xml): | ||||
| @@ -444,8 +522,6 @@ class XMLStream(object): | ||||
|         """ | ||||
|         # To prevent circular dependencies, we must load the matcher | ||||
|         # and handler classes here. | ||||
|         from sleekxmpp.xmlstream.matcher import MatchXMLMask | ||||
|         from sleekxmpp.xmlstream.handler import XMLCallback | ||||
|  | ||||
|         if name is None: | ||||
|             name = 'add_handler_%s' % self.getNewId() | ||||
| @@ -517,6 +593,17 @@ class XMLStream(object): | ||||
|         self.__event_handlers[name] = filter(filter_pointers, | ||||
|                                              self.__event_handlers[name]) | ||||
|  | ||||
|     def event_handled(self, name): | ||||
|         """ | ||||
|         Indicates if an event has any associated handlers. | ||||
|  | ||||
|         Returns the number of registered handlers. | ||||
|  | ||||
|         Arguments: | ||||
|             name -- The name of the event to check. | ||||
|         """ | ||||
|         return len(self.__event_handlers.get(name, [])) | ||||
|  | ||||
|     def event(self, name, data={}, direct=False): | ||||
|         """ | ||||
|         Manually trigger a custom event. | ||||
| @@ -525,13 +612,22 @@ class XMLStream(object): | ||||
|             name     -- The name of the event to trigger. | ||||
|             data     -- Data that will be passed to each event handler. | ||||
|                         Defaults to an empty dictionary. | ||||
|             direct   -- Runs the event directly if True. | ||||
|             direct   -- Runs the event directly if True, skipping the | ||||
|                         event queue. All event handlers will run in the | ||||
|                         same thread. | ||||
|         """ | ||||
|         for handler in self.__event_handlers.get(name, []): | ||||
|             if direct: | ||||
|                 handler[0](copy.copy(data)) | ||||
|                 try: | ||||
|                     handler[0](copy.copy(data)) | ||||
|                 except Exception as e: | ||||
|                     error_msg = 'Error processing event handler: %s' | ||||
|                     log.exception(error_msg % str(handler[0])) | ||||
|                     if hasattr(data, 'exception'): | ||||
|                         data.exception(e) | ||||
|             else: | ||||
|                 self.event_queue.put(('event', handler, copy.copy(data))) | ||||
|  | ||||
|             if handler[2]: | ||||
|                 # If the handler is disposable, we will go ahead and | ||||
|                 # remove it now instead of waiting for it to be | ||||
| @@ -572,7 +668,7 @@ class XMLStream(object): | ||||
|         """ | ||||
|         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. | ||||
|  | ||||
| @@ -586,30 +682,26 @@ class XMLStream(object): | ||||
|                        or a timeout occurs. | ||||
|             timeout -- Time in seconds to wait for a response before | ||||
|                        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'): | ||||
|             mask = mask.xml | ||||
|         data = str(data) | ||||
|         if mask is not None: | ||||
|             logging.warning("Use of send mask waiters is deprecated.") | ||||
|             log.warning("Use of send mask waiters is deprecated.") | ||||
|             wait_for = Waiter("SendWait_%s" % self.new_id(), | ||||
|                               MatchXMLMask(mask)) | ||||
|             self.register_handler(wait_for) | ||||
|         self.send_raw(data) | ||||
|         self.send_raw(data, now) | ||||
|         if mask is not None: | ||||
|             return wait_for.wait(timeout) | ||||
|  | ||||
|     def send_raw(self, data): | ||||
|         """ | ||||
|         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): | ||||
|     def send_xml(self, data, mask=None, timeout=None, now=False): | ||||
|         """ | ||||
|         Send an XML object on the stream, and optionally wait | ||||
|         for a response. | ||||
| @@ -622,8 +714,39 @@ class XMLStream(object): | ||||
|                        or a timeout occurs. | ||||
|             timeout -- Time in seconds to wait for a response before | ||||
|                        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): | ||||
|         """ | ||||
| @@ -641,14 +764,16 @@ class XMLStream(object): | ||||
|                         Event handlers and the send queue will be threaded | ||||
|                         regardless of this parameter's value. | ||||
|         """ | ||||
|         self._thread_excepthook() | ||||
|         self.scheduler.process(threaded=True) | ||||
|  | ||||
|         def start_thread(name, target): | ||||
|             self.__thread[name] = threading.Thread(name=name, target=target) | ||||
|             self.__thread[name].daemon = True | ||||
|             self.__thread[name].start() | ||||
|  | ||||
|         for t in range(0, HANDLER_THREADS): | ||||
|             logging.debug("Starting HANDLER THREAD") | ||||
|             log.debug("Starting HANDLER THREAD") | ||||
|             start_thread('stream_event_handler_%s' % t, self._event_runner) | ||||
|  | ||||
|         start_thread('send_thread', self._send_thread) | ||||
| @@ -675,7 +800,7 @@ class XMLStream(object): | ||||
|             firstrun = False | ||||
|             try: | ||||
|                 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 body of the loop from running until a disconnect | ||||
|                 # occurs. After any reconnection, the stream header will | ||||
| @@ -684,21 +809,23 @@ class XMLStream(object): | ||||
|                     # Ensure the stream header is sent for any | ||||
|                     # new connections. | ||||
|                     if self.is_client: | ||||
|                         self.send_raw(self.stream_header) | ||||
|                         self.send_raw(self.stream_header, now=True) | ||||
|             except KeyboardInterrupt: | ||||
|                 logging.debug("Keyboard Escape Detected in _process") | ||||
|                 log.debug("Keyboard Escape Detected in _process") | ||||
|                 self.stop.set() | ||||
|             except SystemExit: | ||||
|                 logging.debug("SystemExit in _process") | ||||
|                 log.debug("SystemExit in _process") | ||||
|                 self.stop.set() | ||||
|             except Socket.error: | ||||
|                 logging.exception('Socket Error') | ||||
|             except Socket.error as serr: | ||||
|                 self.event('socket_error', serr) | ||||
|                 log.exception('Socket Error') | ||||
|             except: | ||||
|                 if not self.stop.isSet(): | ||||
|                     logging.exception('Connection error.') | ||||
|                     log.exception('Connection error.') | ||||
|             if not self.stop.isSet() and self.auto_reconnect: | ||||
|                 self.reconnect() | ||||
|             else: | ||||
|                 self.event('killed', direct=True) | ||||
|                 self.disconnect() | ||||
|                 self.event_queue.put(('quit', None, None)) | ||||
|         self.scheduler.run = False | ||||
| @@ -710,36 +837,63 @@ class XMLStream(object): | ||||
|         """ | ||||
|         depth = 0 | ||||
|         root = None | ||||
|         for (event, xml) in ET.iterparse(self.filesocket, (b'end', b'start')): | ||||
|             if event == b'start': | ||||
|                 if depth == 0: | ||||
|                     # We have received the start of the root element. | ||||
|                     root = xml | ||||
|                     # Perform any stream initialization actions, such | ||||
|                     # as handshakes. | ||||
|                     self.stream_end_event.clear() | ||||
|                     self.start_stream_handler(root) | ||||
|                 depth += 1 | ||||
|             if event == b'end': | ||||
|                 depth -= 1 | ||||
|                 if depth == 0: | ||||
|                     # The stream's root element has closed, | ||||
|                     # terminating the stream. | ||||
|                     logging.debug("End of stream recieved") | ||||
|                     self.stream_end_event.set() | ||||
|                     return False | ||||
|                 elif depth == 1: | ||||
|                     # We only raise events for stanzas that are direct | ||||
|                     # children of the root element. | ||||
|                     try: | ||||
|                         self.__spawn_event(xml) | ||||
|                     except RestartStream: | ||||
|                         return True | ||||
|                     if root: | ||||
|                         # Keep the root element empty of children to | ||||
|                         # save on memory use. | ||||
|                         root.clear() | ||||
|         logging.debug("Ending read XML loop") | ||||
|         try: | ||||
|             for (event, xml) in ET.iterparse(self.filesocket, | ||||
|                                              (b'end', b'start')): | ||||
|                 if event == b'start': | ||||
|                     if depth == 0: | ||||
|                         # We have received the start of the root element. | ||||
|                         root = xml | ||||
|                         # Perform any stream initialization actions, such | ||||
|                         # as handshakes. | ||||
|                         self.stream_end_event.clear() | ||||
|                         self.start_stream_handler(root) | ||||
|                     depth += 1 | ||||
|                 if event == b'end': | ||||
|                     depth -= 1 | ||||
|                     if depth == 0: | ||||
|                         # The stream's root element has closed, | ||||
|                         # terminating the stream. | ||||
|                         log.debug("End of stream recieved") | ||||
|                         self.stream_end_event.set() | ||||
|                         return False | ||||
|                     elif depth == 1: | ||||
|                         # We only raise events for stanzas that are direct | ||||
|                         # children of the root element. | ||||
|                         try: | ||||
|                             self.__spawn_event(xml) | ||||
|                         except RestartStream: | ||||
|                             return True | ||||
|                         if root: | ||||
|                             # 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") | ||||
|  | ||||
|     def _build_stanza(self, xml, default_ns=None): | ||||
|         """ | ||||
|         Create a stanza object from a given XML object. | ||||
|  | ||||
|         If a specialized stanza type is not found for the XML, then | ||||
|         a generic StanzaBase stanza will be returned. | ||||
|  | ||||
|         Arguments: | ||||
|             xml        -- The XML object to convert into a stanza object. | ||||
|             default_ns -- Optional default namespace to use instead of the | ||||
|                           stream's current default namespace. | ||||
|         """ | ||||
|         if default_ns is None: | ||||
|             default_ns = self.default_ns | ||||
|         stanza_type = StanzaBase | ||||
|         for stanza_class in self.__root_stanza: | ||||
|             if xml.tag == "{%s}%s" % (default_ns, stanza_class.name) or \ | ||||
|                xml.tag == stanza_class.tag_name(): | ||||
|                 stanza_type = stanza_class | ||||
|                 break | ||||
|         stanza = stanza_type(self, xml) | ||||
|         return stanza | ||||
|  | ||||
|     def __spawn_event(self, xml): | ||||
|         """ | ||||
| @@ -750,7 +904,7 @@ class XMLStream(object): | ||||
|         Arguments: | ||||
|             xml -- The XML stanza to analyze. | ||||
|         """ | ||||
|         logging.debug("RECV: %s" % tostring(xml, | ||||
|         log.debug("RECV: %s" % tostring(xml, | ||||
|                                             xmlns=self.default_ns, | ||||
|                                             stream=self)) | ||||
|         # Apply any preprocessing filters. | ||||
| @@ -758,12 +912,7 @@ class XMLStream(object): | ||||
|  | ||||
|         # Convert the raw XML object into a stanza object. If no registered | ||||
|         # stanza type applies, a generic StanzaBase stanza will be used. | ||||
|         stanza_type = StanzaBase | ||||
|         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) | ||||
|         stanza = self._build_stanza(xml) | ||||
|  | ||||
|         # Match the stanza against registered handlers. Handlers marked | ||||
|         # to run "in stream" will be executed immediately; the rest will | ||||
| @@ -771,12 +920,12 @@ class XMLStream(object): | ||||
|         unhandled = True | ||||
|         for handler in self.__handlers: | ||||
|             if handler.match(stanza): | ||||
|                 stanza_copy = stanza_type(self, copy.deepcopy(xml)) | ||||
|                 stanza_copy = copy.copy(stanza) | ||||
|                 handler.prerun(stanza_copy) | ||||
|                 self.event_queue.put(('stanza', handler, stanza_copy)) | ||||
|                 try: | ||||
|                     if handler.check_delete(): | ||||
|                         self.__handlers.pop(self.__handlers.index(handler)) | ||||
|                         self.__handlers.remove(handler) | ||||
|                 except: | ||||
|                     pass  # not thread safe | ||||
|                 unhandled = False | ||||
| @@ -786,6 +935,24 @@ class XMLStream(object): | ||||
|         if unhandled: | ||||
|             stanza.unhandled() | ||||
|  | ||||
|     def _threaded_event_wrapper(self, func, args): | ||||
|         """ | ||||
|         Capture exceptions for event handlers that run | ||||
|         in individual threads. | ||||
|  | ||||
|         Arguments: | ||||
|             func -- The event handler to execute. | ||||
|             args -- Arguments to the event handler. | ||||
|         """ | ||||
|         orig = copy.copy(args[0]) | ||||
|         try: | ||||
|             func(*args) | ||||
|         except Exception as e: | ||||
|             error_msg = 'Error processing event handler: %s' | ||||
|             log.exception(error_msg % str(func)) | ||||
|             if hasattr(orig, 'exception'): | ||||
|                 orig.exception(e) | ||||
|  | ||||
|     def _event_runner(self): | ||||
|         """ | ||||
|         Process the event queue and execute handlers. | ||||
| @@ -795,7 +962,7 @@ class XMLStream(object): | ||||
|         Stream event handlers will all execute in this thread. Custom event | ||||
|         handlers may be spawned in individual threads. | ||||
|         """ | ||||
|         logging.debug("Loading event runner") | ||||
|         log.debug("Loading event runner") | ||||
|         try: | ||||
|             while not self.stop.isSet(): | ||||
|                 try: | ||||
| @@ -807,37 +974,44 @@ class XMLStream(object): | ||||
|  | ||||
|                 etype, handler = event[0:2] | ||||
|                 args = event[2:] | ||||
|                 orig = copy.copy(args[0]) | ||||
|  | ||||
|                 if etype == 'stanza': | ||||
|                     try: | ||||
|                         handler.run(args[0]) | ||||
|                     except Exception as e: | ||||
|                         error_msg = 'Error processing stream handler: %s' | ||||
|                         logging.exception(error_msg % handler.name) | ||||
|                         args[0].exception(e) | ||||
|                         log.exception(error_msg % handler.name) | ||||
|                         orig.exception(e) | ||||
|                 elif etype == 'schedule': | ||||
|                     try: | ||||
|                         logging.debug(args) | ||||
|                         log.debug('Scheduled event: %s' % args) | ||||
|                         handler(*args[0]) | ||||
|                     except: | ||||
|                         logging.exception('Error processing scheduled task') | ||||
|                         log.exception('Error processing scheduled task') | ||||
|                 elif etype == 'event': | ||||
|                     func, threaded, disposable = handler | ||||
|                     orig = copy.copy(args[0]) | ||||
|                     try: | ||||
|                         if threaded: | ||||
|                             x = threading.Thread(name="Event_%s" % str(func), | ||||
|                                                  target=func, | ||||
|                                                  args=args) | ||||
|                             x = threading.Thread( | ||||
|                                     name="Event_%s" % str(func), | ||||
|                                     target=self._threaded_event_wrapper, | ||||
|                                     args=(func, args)) | ||||
|                             x.start() | ||||
|                         else: | ||||
|                             func(*args) | ||||
|                     except: | ||||
|                         logging.exception('Error processing event handler: %s') | ||||
|                     except Exception as e: | ||||
|                         error_msg = 'Error processing event handler: %s' | ||||
|                         log.exception(error_msg % str(func)) | ||||
|                         if hasattr(orig, 'exception'): | ||||
|                             orig.exception(e) | ||||
|                 elif etype == 'quit': | ||||
|                     logging.debug("Quitting event runner thread") | ||||
|                     log.debug("Quitting event runner thread") | ||||
|                     return False | ||||
|         except KeyboardInterrupt: | ||||
|             logging.debug("Keyboard Escape Detected in _event_runner") | ||||
|             log.debug("Keyboard Escape Detected in _event_runner") | ||||
|             self.event('killed', direct=True) | ||||
|             self.disconnect() | ||||
|             return | ||||
|         except SystemExit: | ||||
| @@ -851,21 +1025,68 @@ class XMLStream(object): | ||||
|         """ | ||||
|         try: | ||||
|             while not self.stop.isSet(): | ||||
|                 try: | ||||
|                     data = self.send_queue.get(True, 1) | ||||
|                 except queue.Empty: | ||||
|                     continue | ||||
|                 logging.debug("SEND: %s" % data) | ||||
|                 self.session_started_event.wait() | ||||
|                 if self.__failed_send_stanza is not None: | ||||
|                     data = self.__failed_send_stanza | ||||
|                     self.__failed_send_stanza = None | ||||
|                 else: | ||||
|                     try: | ||||
|                         data = self.send_queue.get(True, 1) | ||||
|                     except queue.Empty: | ||||
|                         continue | ||||
|                 log.debug("SEND: %s" % data) | ||||
|                 try: | ||||
|                     self.socket.send(data.encode('utf-8')) | ||||
|                 except: | ||||
|                     logging.warning("Failed to send %s" % data) | ||||
|                 except Socket.error as serr: | ||||
|                     self.event('socket_error', serr) | ||||
|                     log.warning("Failed to send %s" % data) | ||||
|                     self.__failed_send_stanza = data | ||||
|                     self.disconnect(self.auto_reconnect) | ||||
|         except KeyboardInterrupt: | ||||
|             logging.debug("Keyboard Escape Detected in _send_thread") | ||||
|             log.debug("Keyboard Escape Detected in _send_thread") | ||||
|             self.event('killed', direct=True) | ||||
|             self.disconnect() | ||||
|             return | ||||
|         except SystemExit: | ||||
|             self.disconnect() | ||||
|             self.event_queue.put(('quit', None, None)) | ||||
|             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())) | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user