Compare commits
	
		
			43 Commits
		
	
	
		
			slix-1.2.2
			...
			slix-1.3.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d73f56a7af | ||
|   | 7c7f4308c5 | ||
|   | eab8c265f4 | ||
|   | 80b9cd43b1 | ||
|   | af1f9e08ad | ||
|   | e3fd0af9c8 | ||
|   | 27e23672c1 | ||
|   | b38e229359 | ||
|   | 9a563f1425 | ||
|   | 8b6f5953a7 | ||
|   | 2d2a80c73d | ||
|   | 4dfdd5d8e3 | ||
|   | 1994ed3025 | ||
|   | aaa45846d3 | ||
|   | d7ffcb54eb | ||
|   | c33749e57a | ||
|   | e4107d8b4d | ||
|   | da5cb72d3a | ||
|   | c372bd5168 | ||
|   | cabf623131 | ||
|   | ffc240d5b6 | ||
|   | cc4522d9cd | ||
|   | 5bf69dca76 | ||
|   | 59dad12820 | ||
|   | 007c836296 | ||
|   | 3721bf9f6b | ||
|   | 802949eba8 | ||
|   | 24f35e433f | ||
|   | 22664ee7b8 | ||
|   | 6476cfcde5 | ||
|   | 5bb347e884 | ||
|   | eb1251b919 | ||
|   | 820144c40c | ||
|   | 6034df0a78 | ||
|   | df4012e66d | ||
|   | c372f3071a | ||
|   | 829c8b27b6 | ||
|   | fb3ac78bf9 | ||
|   | ffd9436e5c | ||
|   | bbb1344d79 | ||
|   | 457785b286 | ||
|   | 4847f834bd | ||
|   | 8b06aa1146 | 
							
								
								
									
										8
									
								
								.gitlab-ci.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.gitlab-ci.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| test: | ||||
|   tags: | ||||
|     - docker | ||||
|   image: ubuntu:latest | ||||
|   script: | ||||
|     - apt update | ||||
|     - apt install -y python3 cython3 | ||||
|     - ./run_tests.py | ||||
| @@ -36,7 +36,7 @@ The Slixmpp Boilerplate | ||||
| ------------------------- | ||||
| Projects using Slixmpp tend to follow a basic pattern for setting up client/component | ||||
| connections and configuration. Here is the gist of the boilerplate needed for a Slixmpp | ||||
| based project. See the documetation or examples directory for more detailed archetypes for | ||||
| based project. See the documentation or examples directory for more detailed archetypes for | ||||
| Slixmpp projects:: | ||||
|  | ||||
|     import logging | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| .. _mucbot: | ||||
|  | ||||
| ========================= | ||||
| Mulit-User Chat (MUC) Bot | ||||
| Multi-User Chat (MUC) Bot | ||||
| ========================= | ||||
|  | ||||
| .. note:: | ||||
|   | ||||
| @@ -50,7 +50,7 @@ class ActionBot(slixmpp.ClientXMPP): | ||||
|  | ||||
|         register_stanza_plugin(Iq, Action) | ||||
|  | ||||
|     def start(self, event): | ||||
|     async def start(self, event): | ||||
|         """ | ||||
|         Process the session_start event. | ||||
|  | ||||
| @@ -73,7 +73,7 @@ class ActionBot(slixmpp.ClientXMPP): | ||||
|         """ | ||||
|         self.event('custom_action', iq) | ||||
|  | ||||
|     def _handle_action_event(self, iq): | ||||
|     async def _handle_action_event(self, iq): | ||||
|         """ | ||||
|         Respond to the custom action event. | ||||
|         """ | ||||
| @@ -82,17 +82,20 @@ class ActionBot(slixmpp.ClientXMPP): | ||||
|  | ||||
|         if method == 'is_prime' and param == '2': | ||||
|             print("got message: %s" % iq) | ||||
|             iq.reply() | ||||
|             iq['action']['status'] = 'done' | ||||
|             iq.send() | ||||
|             rep = iq.reply() | ||||
|             rep['action']['status'] = 'done' | ||||
|             await rep.send() | ||||
|         elif method == 'bye': | ||||
|             print("got message: %s" % iq) | ||||
|             rep = iq.reply() | ||||
|             rep['action']['status'] = 'done' | ||||
|             await rep.send() | ||||
|             self.disconnect() | ||||
|         else: | ||||
|             print("got message: %s" % iq) | ||||
|             iq.reply() | ||||
|             iq['action']['status'] = 'error' | ||||
|             iq.send() | ||||
|             rep = iq.reply() | ||||
|             rep['action']['status'] = 'error' | ||||
|             await rep.send() | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     # Setup the command line arguments. | ||||
|   | ||||
| @@ -43,7 +43,7 @@ class ActionUserBot(slixmpp.ClientXMPP): | ||||
|  | ||||
|         register_stanza_plugin(Iq, Action) | ||||
|  | ||||
|     def start(self, event): | ||||
|     async def start(self, event): | ||||
|         """ | ||||
|         Process the session_start event. | ||||
|  | ||||
| @@ -57,11 +57,11 @@ class ActionUserBot(slixmpp.ClientXMPP): | ||||
|                      data. | ||||
|         """ | ||||
|         self.send_presence() | ||||
|         self.get_roster() | ||||
|         await self.get_roster() | ||||
|  | ||||
|         self.send_custom_iq() | ||||
|         await self.send_custom_iq() | ||||
|  | ||||
|     def send_custom_iq(self): | ||||
|     async def send_custom_iq(self): | ||||
|         """Create and send two custom actions. | ||||
|  | ||||
|         If the first action was successful, then send | ||||
| @@ -74,14 +74,14 @@ class ActionUserBot(slixmpp.ClientXMPP): | ||||
|         iq['action']['param'] = '2' | ||||
|  | ||||
|         try: | ||||
|             resp = iq.send() | ||||
|             resp = await iq.send() | ||||
|             if resp['action']['status'] == 'done': | ||||
|                 #sending bye | ||||
|                 iq2 = self.Iq() | ||||
|                 iq2['to'] = self.action_provider | ||||
|                 iq2['type'] = 'set' | ||||
|                 iq2['action']['method'] = 'bye' | ||||
|                 iq2.send(block=False) | ||||
|                 await iq2.send() | ||||
|  | ||||
|                 self.disconnect() | ||||
|         except XMPPError: | ||||
|   | ||||
| @@ -55,8 +55,8 @@ class GTalkBot(slixmpp.ClientXMPP): | ||||
|             cert.verify('talk.google.com', der_cert) | ||||
|             logging.debug("CERT: Found GTalk certificate") | ||||
|         except cert.CertificateError as err: | ||||
|             log.error(err.message) | ||||
|             self.disconnect(send_close=False) | ||||
|             logging.error(err.message) | ||||
|             self.disconnect() | ||||
|  | ||||
|     def start(self, event): | ||||
|         """ | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|  | ||||
| from slixmpp import ClientXMPP | ||||
|  | ||||
| from optparse import OptionParser | ||||
| from argparse import ArgumentParser | ||||
| import logging | ||||
| import getpass | ||||
|  | ||||
| @@ -23,7 +23,7 @@ class HTTPOverXMPPClient(ClientXMPP): | ||||
|         ClientXMPP.__init__(self, jid, password) | ||||
|         self.register_plugin('xep_0332')    # HTTP over XMPP Transport | ||||
|         self.add_event_handler( | ||||
|             'session_start', self.session_start, threaded=True | ||||
|             'session_start', self.session_start | ||||
|         ) | ||||
|         self.add_event_handler('http_request', self.http_request_received) | ||||
|         self.add_event_handler('http_response', self.http_response_received) | ||||
| @@ -58,40 +58,40 @@ if __name__ == '__main__': | ||||
|     # ./http_over_xmpp.py -J <jid> -P <pwd> -i <ip> -p <port> [-v] | ||||
|     # | ||||
|  | ||||
|     parser = OptionParser() | ||||
|     parser = ArgumentParser() | ||||
|  | ||||
|     # Output verbosity options. | ||||
|     parser.add_option( | ||||
|     parser.add_argument( | ||||
|         '-v', '--verbose', help='set logging to DEBUG', action='store_const', | ||||
|         dest='loglevel', const=logging.DEBUG, default=logging.ERROR | ||||
|     ) | ||||
|  | ||||
|     # JID and password options. | ||||
|     parser.add_option('-J', '--jid', dest='jid', help='JID') | ||||
|     parser.add_option('-P', '--password', dest='password', help='Password') | ||||
|     parser.add_argument('-J', '--jid', dest='jid', help='JID') | ||||
|     parser.add_argument('-P', '--password', dest='password', help='Password') | ||||
|  | ||||
|     # XMPP server ip and port options. | ||||
|     parser.add_option( | ||||
|     parser.add_argument( | ||||
|         '-i', '--ipaddr', dest='ipaddr', | ||||
|         help='IP Address of the XMPP server', default=None | ||||
|     ) | ||||
|     parser.add_option( | ||||
|     parser.add_argument( | ||||
|         '-p', '--port', dest='port', | ||||
|         help='Port of the XMPP server', default=None | ||||
|     ) | ||||
|  | ||||
|     opts, args = parser.parse_args() | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     # Setup logging. | ||||
|     logging.basicConfig(level=opts.loglevel, | ||||
|     logging.basicConfig(level=args.loglevel, | ||||
|                         format='%(levelname)-8s %(message)s') | ||||
|  | ||||
|     if opts.jid is None: | ||||
|         opts.jid = input('Username: ') | ||||
|     if opts.password is None: | ||||
|         opts.password = getpass.getpass('Password: ') | ||||
|     if args.jid is None: | ||||
|         args.jid = input('Username: ') | ||||
|     if args.password is None: | ||||
|         args.password = getpass.getpass('Password: ') | ||||
|  | ||||
|     xmpp = HTTPOverXMPPClient(opts.jid, opts.password) | ||||
|     xmpp = HTTPOverXMPPClient(args.jid, args.password) | ||||
|     xmpp.connect() | ||||
|     xmpp.process() | ||||
|  | ||||
|   | ||||
							
								
								
									
										98
									
								
								examples/mam.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										98
									
								
								examples/mam.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| #!/usr/bin/env python3 | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2017 Mathieu Pasquet | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| import logging | ||||
| from getpass import getpass | ||||
| from argparse import ArgumentParser | ||||
|  | ||||
| import slixmpp | ||||
| from slixmpp.exceptions import XMPPError | ||||
| from slixmpp import asyncio | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class MAM(slixmpp.ClientXMPP): | ||||
|  | ||||
|     """ | ||||
|     A basic client fetching mam archive messages | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, jid, password, remote_jid, start): | ||||
|         slixmpp.ClientXMPP.__init__(self, jid, password) | ||||
|         self.remote_jid = remote_jid | ||||
|         self.start_date = start | ||||
|  | ||||
|         self.add_event_handler("session_start", self.start) | ||||
|  | ||||
|     async def start(self, *args): | ||||
|         """ | ||||
|         Fetch mam results for the specified JID. | ||||
|         Use RSM to paginate the results. | ||||
|         """ | ||||
|         results = self.plugin['xep_0313'].retrieve(jid=self.remote_jid, iterator=True, rsm={'max': 10}, start=self.start_date) | ||||
|         page = 1 | ||||
|         async for rsm in results: | ||||
|             print('Page %d' % page) | ||||
|             for msg in rsm['mam']['results']: | ||||
|                 forwarded = msg['mam_result']['forwarded'] | ||||
|                 timestamp = forwarded['delay']['stamp'] | ||||
|                 message = forwarded['stanza'] | ||||
|                 print('[%s] %s: %s' % (timestamp, message['from'], message['body'])) | ||||
|             page += 1 | ||||
|         self.disconnect() | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     # Setup the command line arguments. | ||||
|     parser = ArgumentParser() | ||||
|     parser.add_argument("-q","--quiet", help="set logging to ERROR", | ||||
|                         action="store_const", | ||||
|                         dest="loglevel", | ||||
|                         const=logging.ERROR, | ||||
|                         default=logging.INFO) | ||||
|     parser.add_argument("-d","--debug", help="set logging to DEBUG", | ||||
|                         action="store_const", | ||||
|                         dest="loglevel", | ||||
|                         const=logging.DEBUG, | ||||
|                         default=logging.INFO) | ||||
|  | ||||
|     # JID and password options. | ||||
|     parser.add_argument("-j", "--jid", dest="jid", | ||||
|                         help="JID to use") | ||||
|     parser.add_argument("-p", "--password", dest="password", | ||||
|                         help="password to use") | ||||
|  | ||||
|     # Other options | ||||
|     parser.add_argument("-r", "--remote-jid", dest="remote_jid", | ||||
|                         help="Remote JID") | ||||
|     parser.add_argument("--start", help="Start date", default='2017-09-20T12:00:00Z') | ||||
|  | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     # Setup logging. | ||||
|     logging.basicConfig(level=args.loglevel, | ||||
|                         format='%(levelname)-8s %(message)s') | ||||
|  | ||||
|     if args.jid is None: | ||||
|         args.jid = input("Username: ") | ||||
|     if args.password is None: | ||||
|         args.password = getpass("Password: ") | ||||
|     if args.remote_jid is None: | ||||
|         args.remote_jid = input("Remote JID: ") | ||||
|     if args.start is None: | ||||
|         args.start = input("Start time: ") | ||||
|  | ||||
|     xmpp = MAM(args.jid, args.password, args.remote_jid, args.start) | ||||
|     xmpp.register_plugin('xep_0313') | ||||
|  | ||||
|     # Connect to the XMPP server and start processing XMPP stanzas. | ||||
|     xmpp.connect() | ||||
|     xmpp.process(forever=False) | ||||
							
								
								
									
										120
									
								
								examples/markup.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										120
									
								
								examples/markup.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| #!/usr/bin/env python3 | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2010  Nathanael C. Fritz | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| import logging | ||||
| from getpass import getpass | ||||
| from argparse import ArgumentParser | ||||
|  | ||||
| import slixmpp | ||||
| from slixmpp.plugins.xep_0394 import stanza as markup_stanza | ||||
|  | ||||
|  | ||||
| class EchoBot(slixmpp.ClientXMPP): | ||||
|  | ||||
|     """ | ||||
|     A simple Slixmpp bot that will echo messages it | ||||
|     receives, along with a short thank you message. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, jid, password): | ||||
|         slixmpp.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 initialize | ||||
|         # our roster. | ||||
|         self.add_event_handler("session_start", self.start) | ||||
|  | ||||
|         # The message event is triggered whenever a message | ||||
|         # stanza is received. Be aware that that includes | ||||
|         # MUC messages and error messages. | ||||
|         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 initial | ||||
|         presence stanza. | ||||
|  | ||||
|         Arguments: | ||||
|             event -- An empty dictionary. The session_start | ||||
|                      event does not provide any additional | ||||
|                      data. | ||||
|         """ | ||||
|         self.send_presence() | ||||
|         self.get_roster() | ||||
|  | ||||
|     def message(self, msg): | ||||
|         """ | ||||
|         Process incoming message stanzas. Be aware that this also | ||||
|         includes MUC messages and error messages. It is usually | ||||
|         a good idea to check the messages's type before processing | ||||
|         or sending replies. | ||||
|  | ||||
|         Arguments: | ||||
|             msg -- The received message stanza. See the documentation | ||||
|                    for stanza objects and the Message stanza to see | ||||
|                    how it may be used. | ||||
|         """ | ||||
|         body = msg['body'] | ||||
|         new_body = self['xep_0394'].to_plain_text(body, msg['markup']) | ||||
|         xhtml = self['xep_0394'].to_xhtml_im(body, msg['markup']) | ||||
|         print('Plain text:', new_body) | ||||
|         print('XHTML-IM:', xhtml['body']) | ||||
|         message = msg.reply() | ||||
|         message['body'] = new_body | ||||
|         message['html']['body'] = xhtml['body'] | ||||
|         self.send(message) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     # Setup the command line arguments. | ||||
|     parser = ArgumentParser(description=EchoBot.__doc__) | ||||
|  | ||||
|     # Output verbosity options. | ||||
|     parser.add_argument("-q", "--quiet", help="set logging to ERROR", | ||||
|                         action="store_const", dest="loglevel", | ||||
|                         const=logging.ERROR, default=logging.INFO) | ||||
|     parser.add_argument("-d", "--debug", help="set logging to DEBUG", | ||||
|                         action="store_const", dest="loglevel", | ||||
|                         const=logging.DEBUG, default=logging.INFO) | ||||
|  | ||||
|     # JID and password options. | ||||
|     parser.add_argument("-j", "--jid", dest="jid", | ||||
|                         help="JID to use") | ||||
|     parser.add_argument("-p", "--password", dest="password", | ||||
|                         help="password to use") | ||||
|  | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     # Setup logging. | ||||
|     logging.basicConfig(level=args.loglevel, | ||||
|                         format='%(levelname)-8s %(message)s') | ||||
|  | ||||
|     if args.jid is None: | ||||
|         args.jid = input("Username: ") | ||||
|     if args.password is None: | ||||
|         args.password = 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(args.jid, args.password) | ||||
|     xmpp.register_plugin('xep_0030') # Service Discovery | ||||
|     xmpp.register_plugin('xep_0199') # XMPP Ping | ||||
|     xmpp.register_plugin('xep_0394') # Message Markup | ||||
|  | ||||
|     # Connect to the XMPP server and start processing XMPP stanzas. | ||||
|     xmpp.connect() | ||||
|     xmpp.process() | ||||
							
								
								
									
										26
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								setup.py
									
									
									
									
									
								
							| @@ -9,7 +9,7 @@ | ||||
|  | ||||
| import os | ||||
| from pathlib import Path | ||||
| from subprocess import call, DEVNULL | ||||
| from subprocess import call, DEVNULL, check_output, CalledProcessError | ||||
| from tempfile import TemporaryFile | ||||
| try: | ||||
|     from setuptools import setup | ||||
| @@ -30,23 +30,39 @@ CLASSIFIERS = [ | ||||
|     'License :: OSI Approved :: MIT License', | ||||
|     'Programming Language :: Python', | ||||
|     'Programming Language :: Python :: 3.4', | ||||
|     'Programming Language :: Python :: 3.5', | ||||
|     'Programming Language :: Python :: 3.6', | ||||
|     'Topic :: Internet :: XMPP', | ||||
|     'Topic :: Software Development :: Libraries :: Python Modules', | ||||
| ] | ||||
|  | ||||
| packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')] | ||||
|  | ||||
| def check_include(header): | ||||
|     command = [os.environ.get('CC', 'cc'), '-E', '-'] | ||||
| def check_include(library_name, header): | ||||
|     command = [os.environ.get('PKG_CONFIG', 'pkg-config'), '--cflags', library_name] | ||||
|     try: | ||||
|         cflags = check_output(command).decode('utf-8').split() | ||||
|     except FileNotFoundError: | ||||
|         print('pkg-config not found.') | ||||
|         return False | ||||
|     except CalledProcessError: | ||||
|         # pkg-config already prints the missing libraries on stderr. | ||||
|         return False | ||||
|     command = [os.environ.get('CC', 'cc')] + cflags + ['-E', '-'] | ||||
|     with TemporaryFile('w+') as c_file: | ||||
|         c_file.write('#include <%s>' % header) | ||||
|         c_file.seek(0) | ||||
|         try: | ||||
|             return call(command, stdin=c_file, stdout=DEVNULL, stderr=DEVNULL) == 0 | ||||
|         except FileNotFoundError: | ||||
|             print('%s headers not found.' % library_name) | ||||
|             return False | ||||
|  | ||||
| HAS_PYTHON_HEADERS = check_include('python3', 'Python.h') | ||||
| HAS_STRINGPREP_HEADERS = check_include('libidn', 'stringprep.h') | ||||
|  | ||||
| ext_modules = None | ||||
| if check_include('stringprep.h'): | ||||
| if HAS_PYTHON_HEADERS and HAS_STRINGPREP_HEADERS: | ||||
|     try: | ||||
|         from Cython.Build import cythonize | ||||
|     except ImportError: | ||||
| @@ -54,7 +70,7 @@ if check_include('stringprep.h'): | ||||
|     else: | ||||
|         ext_modules = cythonize('slixmpp/stringprep.pyx') | ||||
| else: | ||||
|     print('libidn-dev not found, falling back to the slow stringprep module.') | ||||
|     print('Falling back to the slow stringprep module.') | ||||
|  | ||||
| setup( | ||||
|     name="slixmpp", | ||||
|   | ||||
| @@ -15,6 +15,7 @@ | ||||
| import asyncio | ||||
| import logging | ||||
|  | ||||
| from slixmpp.jid import JID | ||||
| from slixmpp.stanza import StreamFeatures | ||||
| from slixmpp.basexmpp import BaseXMPP | ||||
| from slixmpp.exceptions import XMPPError | ||||
| @@ -110,7 +111,13 @@ class ClientXMPP(BaseXMPP): | ||||
|                      self._handle_stream_features)) | ||||
|         def roster_push_filter(iq): | ||||
|             from_ = iq['from'] | ||||
|             if from_ and from_ != self.boundjid.bare: | ||||
|             if from_ and from_ != JID('') and from_ != self.boundjid.bare: | ||||
|                 reply = iq.reply() | ||||
|                 reply['type'] = 'error' | ||||
|                 reply['error']['type'] = 'cancel' | ||||
|                 reply['error']['code'] = 503 | ||||
|                 reply['error']['condition'] = 'service-unavailable' | ||||
|                 reply.send() | ||||
|                 return | ||||
|             self.event('roster_update', iq) | ||||
|         self.register_handler( | ||||
| @@ -248,7 +255,7 @@ class ClientXMPP(BaseXMPP): | ||||
|                 orig_cb(resp) | ||||
|             callback = wrapped | ||||
|  | ||||
|         iq.send(callback, timeout, timeout_callback) | ||||
|         return iq.send(callback, timeout, timeout_callback) | ||||
|  | ||||
|     def _reset_connection_state(self, event=None): | ||||
|         #TODO: Use stream state here | ||||
|   | ||||
							
								
								
									
										47
									
								
								slixmpp/plugins/google/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								slixmpp/plugins/google/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.plugins.base import register_plugin, BasePlugin | ||||
|  | ||||
| from slixmpp.plugins.google.gmail import Gmail | ||||
| from slixmpp.plugins.google.auth import GoogleAuth | ||||
| from slixmpp.plugins.google.settings import GoogleSettings | ||||
| from slixmpp.plugins.google.nosave import GoogleNoSave | ||||
|  | ||||
|  | ||||
| class Google(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     Google: Custom GTalk Features | ||||
|  | ||||
|     Also see: <https://developers.google.com/talk/jep_extensions/extensions> | ||||
|     """ | ||||
|  | ||||
|     name = 'google' | ||||
|     description = 'Google: Custom GTalk Features' | ||||
|     dependencies = set([ | ||||
|         'gmail', | ||||
|         'google_settings', | ||||
|         'google_nosave', | ||||
|         'google_auth' | ||||
|     ]) | ||||
|  | ||||
|     def __getitem__(self, attr): | ||||
|         if attr in ('settings', 'nosave', 'auth'): | ||||
|             return self.xmpp['google_%s' % attr] | ||||
|         elif attr == 'gmail': | ||||
|             return self.xmpp['gmail'] | ||||
|         else: | ||||
|             raise KeyError(attr) | ||||
|  | ||||
|  | ||||
| register_plugin(Gmail) | ||||
| register_plugin(GoogleAuth) | ||||
| register_plugin(GoogleSettings) | ||||
| register_plugin(GoogleNoSave) | ||||
| register_plugin(Google) | ||||
							
								
								
									
										10
									
								
								slixmpp/plugins/google/auth/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								slixmpp/plugins/google/auth/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.plugins.google.auth import stanza | ||||
| from slixmpp.plugins.google.auth.auth import GoogleAuth | ||||
							
								
								
									
										47
									
								
								slixmpp/plugins/google/auth/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								slixmpp/plugins/google/auth/auth.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.xmlstream import register_stanza_plugin | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.plugins.google.auth import stanza | ||||
|  | ||||
|  | ||||
| class GoogleAuth(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     Google: Auth Extensions (JID Domain Discovery, OAuth2) | ||||
|  | ||||
|     Also see: | ||||
|         <https://developers.google.com/talk/jep_extensions/jid_domain_change> | ||||
|         <https://developers.google.com/talk/jep_extensions/oauth> | ||||
|     """ | ||||
|  | ||||
|     name = 'google_auth' | ||||
|     description = 'Google: Auth Extensions (JID Domain Discovery, OAuth2)' | ||||
|     dependencies = set(['feature_mechanisms']) | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         self.xmpp.namespace_map['http://www.google.com/talk/protocol/auth'] = 'ga' | ||||
|  | ||||
|         register_stanza_plugin(self.xmpp['feature_mechanisms'].stanza.Auth, | ||||
|                                stanza.GoogleAuth) | ||||
|  | ||||
|         self.xmpp.add_filter('out', self._auth) | ||||
|  | ||||
|     def plugin_end(self): | ||||
|         self.xmpp.del_filter('out', self._auth) | ||||
|  | ||||
|     def _auth(self, stanza): | ||||
|         if isinstance(stanza, self.xmpp['feature_mechanisms'].stanza.Auth): | ||||
|             stanza.stream = self.xmpp | ||||
|             stanza['google']['client_uses_full_bind_result'] = True | ||||
|             if stanza['mechanism'] == 'X-OAUTH2': | ||||
|                 stanza['google']['service'] = 'oauth2' | ||||
|             print(stanza) | ||||
|         return stanza | ||||
							
								
								
									
										10
									
								
								slixmpp/plugins/google/gmail/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								slixmpp/plugins/google/gmail/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.plugins.google.gmail import stanza | ||||
| from slixmpp.plugins.google.gmail.notifications import Gmail | ||||
							
								
								
									
										101
									
								
								slixmpp/plugins/google/gmail/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								slixmpp/plugins/google/gmail/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.xmlstream import ElementBase, register_stanza_plugin | ||||
|  | ||||
|  | ||||
| class GmailQuery(ElementBase): | ||||
|     namespace = 'google:mail:notify' | ||||
|     name = 'query' | ||||
|     plugin_attrib = 'gmail' | ||||
|     interfaces = set(['newer_than_time', 'newer_than_tid', 'search']) | ||||
|  | ||||
|     def get_search(self): | ||||
|         return self._get_attr('q', '') | ||||
|  | ||||
|     def set_search(self, search): | ||||
|         self._set_attr('q', search) | ||||
|  | ||||
|     def del_search(self): | ||||
|         self._del_attr('q') | ||||
|  | ||||
|     def get_newer_than_time(self): | ||||
|         return self._get_attr('newer-than-time', '') | ||||
|  | ||||
|     def set_newer_than_time(self, value): | ||||
|         self._set_attr('newer-than-time', value) | ||||
|  | ||||
|     def del_newer_than_time(self): | ||||
|         self._del_attr('newer-than-time') | ||||
|  | ||||
|     def get_newer_than_tid(self): | ||||
|         return self._get_attr('newer-than-tid', '') | ||||
|  | ||||
|     def set_newer_than_tid(self, value): | ||||
|         self._set_attr('newer-than-tid', value) | ||||
|  | ||||
|     def del_newer_than_tid(self): | ||||
|         self._del_attr('newer-than-tid') | ||||
|  | ||||
|  | ||||
| class MailBox(ElementBase): | ||||
|     namespace = 'google:mail:notify' | ||||
|     name = 'mailbox' | ||||
|     plugin_attrib = 'gmail_messages' | ||||
|     interfaces = set(['result_time', 'url', 'matched', 'estimate']) | ||||
|  | ||||
|     def get_matched(self): | ||||
|         return self._get_attr('total-matched', '') | ||||
|  | ||||
|     def get_estimate(self): | ||||
|         return self._get_attr('total-estimate', '') == '1' | ||||
|  | ||||
|     def get_result_time(self): | ||||
|         return self._get_attr('result-time', '') | ||||
|  | ||||
|  | ||||
| class MailThread(ElementBase): | ||||
|     namespace = 'google:mail:notify' | ||||
|     name = 'mail-thread-info' | ||||
|     plugin_attrib = 'thread' | ||||
|     plugin_multi_attrib = 'threads' | ||||
|     interfaces = set(['tid', 'participation', 'messages', 'date', | ||||
|                       'senders', 'url', 'labels', 'subject', 'snippet']) | ||||
|     sub_interfaces = set(['labels', 'subject', 'snippet']) | ||||
|  | ||||
|     def get_senders(self): | ||||
|         result = [] | ||||
|         senders = self.xml.findall('{%s}senders/{%s}sender' % ( | ||||
|             self.namespace, self.namespace)) | ||||
|  | ||||
|         for sender in senders: | ||||
|             result.append(MailSender(xml=sender)) | ||||
|  | ||||
|         return result | ||||
|  | ||||
|  | ||||
| class MailSender(ElementBase): | ||||
|     namespace = 'google:mail:notify' | ||||
|     name = 'sender' | ||||
|     plugin_attrib = name | ||||
|     interfaces = set(['address', 'name', 'originator', 'unread']) | ||||
|  | ||||
|     def get_originator(self): | ||||
|         return self.xml.attrib.get('originator', '0') == '1' | ||||
|  | ||||
|     def get_unread(self): | ||||
|         return self.xml.attrib.get('unread', '0') == '1' | ||||
|  | ||||
|  | ||||
| class NewMail(ElementBase): | ||||
|     namespace = 'google:mail:notify' | ||||
|     name = 'new-mail' | ||||
|     plugin_attrib = 'gmail_notification' | ||||
|  | ||||
|  | ||||
| register_stanza_plugin(MailBox, MailThread, iterable=True) | ||||
							
								
								
									
										10
									
								
								slixmpp/plugins/google/nosave/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								slixmpp/plugins/google/nosave/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.plugins.google.nosave import stanza | ||||
| from slixmpp.plugins.google.nosave.nosave import GoogleNoSave | ||||
							
								
								
									
										78
									
								
								slixmpp/plugins/google/nosave/nosave.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								slixmpp/plugins/google/nosave/nosave.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.stanza import Iq, Message | ||||
| from slixmpp.xmlstream.handler import Callback | ||||
| from slixmpp.xmlstream.matcher import StanzaPath | ||||
| from slixmpp.xmlstream import register_stanza_plugin | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.plugins.google.nosave import stanza | ||||
|  | ||||
|  | ||||
| class GoogleNoSave(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     Google: Off the Record Chats | ||||
|  | ||||
|     NOTE: This is NOT an encryption method. | ||||
|  | ||||
|     Also see <https://developers.google.com/talk/jep_extensions/otr>. | ||||
|     """ | ||||
|  | ||||
|     name = 'google_nosave' | ||||
|     description = 'Google: Off the Record Chats' | ||||
|     dependencies = set(['google_settings']) | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         register_stanza_plugin(Message, stanza.NoSave) | ||||
|         register_stanza_plugin(Iq, stanza.NoSaveQuery) | ||||
|  | ||||
|         self.xmpp.register_handler( | ||||
|                 Callback('Google Nosave', | ||||
|                     StanzaPath('iq@type=set/google_nosave'), | ||||
|                     self._handle_nosave_change)) | ||||
|  | ||||
|     def plugin_end(self): | ||||
|         self.xmpp.remove_handler('Google Nosave') | ||||
|  | ||||
|     def enable(self, jid=None, timeout=None, callback=None): | ||||
|         if jid is None: | ||||
|             self.xmpp['google_settings'].update({'archiving_enabled': False}, | ||||
|                     timeout=timeout, callback=callback) | ||||
|         else: | ||||
|             iq = self.xmpp.Iq() | ||||
|             iq['type'] = 'set' | ||||
|             iq['google_nosave']['item']['jid'] = jid | ||||
|             iq['google_nosave']['item']['value'] = True | ||||
|             return iq.send(timeout=timeout, callback=callback) | ||||
|  | ||||
|     def disable(self, jid=None, timeout=None, callback=None): | ||||
|         if jid is None: | ||||
|             self.xmpp['google_settings'].update({'archiving_enabled': True}, | ||||
|                     timeout=timeout, callback=callback) | ||||
|         else: | ||||
|             iq = self.xmpp.Iq() | ||||
|             iq['type'] = 'set' | ||||
|             iq['google_nosave']['item']['jid'] = jid | ||||
|             iq['google_nosave']['item']['value'] = False | ||||
|             return iq.send(timeout=timeout, callback=callback) | ||||
|  | ||||
|     def get(self, timeout=None, callback=None): | ||||
|         iq = self.xmpp.Iq() | ||||
|         iq['type'] = 'get' | ||||
|         iq.enable('google_nosave') | ||||
|         return iq.send(timeout=timeout, callback=callback) | ||||
|  | ||||
|     def _handle_nosave_change(self, iq): | ||||
|         reply = self.xmpp.Iq() | ||||
|         reply['type'] = 'result' | ||||
|         reply['id'] = iq['id'] | ||||
|         reply['to'] = iq['from'] | ||||
|         reply.send() | ||||
|         self.xmpp.event('google_nosave_change', iq) | ||||
							
								
								
									
										10
									
								
								slixmpp/plugins/google/settings/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								slixmpp/plugins/google/settings/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.plugins.google.settings import stanza | ||||
| from slixmpp.plugins.google.settings.settings import GoogleSettings | ||||
							
								
								
									
										110
									
								
								slixmpp/plugins/google/settings/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								slixmpp/plugins/google/settings/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.xmlstream import ET, ElementBase | ||||
|  | ||||
|  | ||||
| class UserSettings(ElementBase): | ||||
|     name = 'usersetting' | ||||
|     namespace = 'google:setting' | ||||
|     plugin_attrib = 'google_settings' | ||||
|     interfaces = set(['auto_accept_suggestions', | ||||
|                       'mail_notifications', | ||||
|                       'archiving_enabled', | ||||
|                       'gmail', | ||||
|                       'email_verified', | ||||
|                       'domain_privacy_notice', | ||||
|                       'display_name']) | ||||
|  | ||||
|     def _get_setting(self, setting): | ||||
|         xml = self.xml.find('{%s}%s' % (self.namespace, setting)) | ||||
|         if xml is not None: | ||||
|             return xml.attrib.get('value', '') == 'true' | ||||
|         return False | ||||
|  | ||||
|     def _set_setting(self, setting, value): | ||||
|         self._del_setting(setting) | ||||
|         if value in (True, False): | ||||
|             xml = ET.Element('{%s}%s' % (self.namespace, setting)) | ||||
|             xml.attrib['value'] = 'true' if value else 'false' | ||||
|             self.xml.append(xml) | ||||
|  | ||||
|     def _del_setting(self, setting): | ||||
|         xml = self.xml.find('{%s}%s' % (self.namespace, setting)) | ||||
|         if xml is not None: | ||||
|             self.xml.remove(xml) | ||||
|  | ||||
|     def get_display_name(self): | ||||
|         xml = self.xml.find('{%s}%s' % (self.namespace, 'displayname')) | ||||
|         if xml is not None: | ||||
|             return xml.attrib.get('value', '') | ||||
|         return '' | ||||
|  | ||||
|     def set_display_name(self, value): | ||||
|         self._del_setting(setting) | ||||
|         if value: | ||||
|             xml = ET.Element('{%s}%s' % (self.namespace, 'displayname')) | ||||
|             xml.attrib['value'] = value | ||||
|             self.xml.append(xml) | ||||
|  | ||||
|     def del_display_name(self): | ||||
|         self._del_setting('displayname') | ||||
|  | ||||
|     def get_auto_accept_suggestions(self): | ||||
|         return self._get_setting('autoacceptsuggestions') | ||||
|  | ||||
|     def get_mail_notifications(self): | ||||
|         return self._get_setting('mailnotifications') | ||||
|  | ||||
|     def get_archiving_enabled(self): | ||||
|         return self._get_setting('archivingenabled') | ||||
|  | ||||
|     def get_gmail(self): | ||||
|         return self._get_setting('gmail') | ||||
|  | ||||
|     def get_email_verified(self): | ||||
|         return self._get_setting('emailverified') | ||||
|  | ||||
|     def get_domain_privacy_notice(self): | ||||
|         return self._get_setting('domainprivacynotice') | ||||
|  | ||||
|     def set_auto_accept_suggestions(self, value): | ||||
|         self._set_setting('autoacceptsuggestions', value) | ||||
|  | ||||
|     def set_mail_notifications(self, value): | ||||
|         self._set_setting('mailnotifications', value) | ||||
|  | ||||
|     def set_archiving_enabled(self, value): | ||||
|         self._set_setting('archivingenabled', value) | ||||
|  | ||||
|     def set_gmail(self, value): | ||||
|         self._set_setting('gmail', value) | ||||
|  | ||||
|     def set_email_verified(self, value): | ||||
|         self._set_setting('emailverified', value) | ||||
|  | ||||
|     def set_domain_privacy_notice(self, value): | ||||
|         self._set_setting('domainprivacynotice', value) | ||||
|  | ||||
|     def del_auto_accept_suggestions(self): | ||||
|         self._del_setting('autoacceptsuggestions') | ||||
|  | ||||
|     def del_mail_notifications(self): | ||||
|         self._del_setting('mailnotifications') | ||||
|  | ||||
|     def del_archiving_enabled(self): | ||||
|         self._del_setting('archivingenabled') | ||||
|  | ||||
|     def del_gmail(self): | ||||
|         self._del_setting('gmail') | ||||
|  | ||||
|     def del_email_verified(self): | ||||
|         self._del_setting('emailverified') | ||||
|  | ||||
|     def del_domain_privacy_notice(self): | ||||
|         self._del_setting('domainprivacynotice') | ||||
| @@ -66,10 +66,11 @@ class StaticDisco(object): | ||||
|         if isinstance(ifrom, JID): | ||||
|             ifrom = ifrom.full | ||||
|         if (jid, node, ifrom) not in self.nodes: | ||||
|             self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(), | ||||
|                                        'items': DiscoItems()} | ||||
|             self.nodes[(jid, node, ifrom)]['info']['node'] = node | ||||
|             self.nodes[(jid, node, ifrom)]['items']['node'] = node | ||||
|             new_node = {'info': DiscoInfo(), 'items': DiscoItems()} | ||||
|             new_node['info']['node'] = node | ||||
|             new_node['items']['node'] = node | ||||
|             self.nodes[(jid, node, ifrom)] = new_node | ||||
|         return self.nodes[(jid, node, ifrom)] | ||||
|  | ||||
|     def get_node(self, jid=None, node=None, ifrom=None): | ||||
|         if jid is None: | ||||
| @@ -208,8 +209,8 @@ class StaticDisco(object): | ||||
|  | ||||
|         The data parameter is a disco#info substanza. | ||||
|         """ | ||||
|         self.add_node(jid, node) | ||||
|         self.get_node(jid, node)['info'] = data | ||||
|         new_node = self.add_node(jid, node) | ||||
|         new_node['info'] = data | ||||
|  | ||||
|     def del_info(self, jid, node, ifrom, data): | ||||
|         """ | ||||
| @@ -242,8 +243,8 @@ class StaticDisco(object): | ||||
|             items -- A set of items in tuple format. | ||||
|         """ | ||||
|         items = data.get('items', set()) | ||||
|         self.add_node(jid, node) | ||||
|         self.get_node(jid, node)['items']['items'] = items | ||||
|         new_node = self.add_node(jid, node) | ||||
|         new_node['items']['items'] = items | ||||
|  | ||||
|     def del_items(self, jid, node, ifrom, data): | ||||
|         """ | ||||
| @@ -264,8 +265,8 @@ class StaticDisco(object): | ||||
|             name     -- Optional human readable name for this identity. | ||||
|             lang     -- Optional standard xml:lang value. | ||||
|         """ | ||||
|         self.add_node(jid, node) | ||||
|         self.get_node(jid, node)['info'].add_identity( | ||||
|         new_node = self.add_node(jid, node) | ||||
|         new_node['info'].add_identity( | ||||
|                 data.get('category', ''), | ||||
|                 data.get('itype', ''), | ||||
|                 data.get('name', None), | ||||
| @@ -280,8 +281,8 @@ class StaticDisco(object): | ||||
|                             (category, type, name, lang) | ||||
|         """ | ||||
|         identities = data.get('identities', set()) | ||||
|         self.add_node(jid, node) | ||||
|         self.get_node(jid, node)['info']['identities'] = identities | ||||
|         new_node = self.add_node(jid, node) | ||||
|         new_node['info']['identities'] = identities | ||||
|  | ||||
|     def del_identity(self, jid, node, ifrom, data): | ||||
|         """ | ||||
| @@ -316,8 +317,8 @@ class StaticDisco(object): | ||||
|         The data parameter should include: | ||||
|             feature -- The namespace of the supported feature. | ||||
|         """ | ||||
|         self.add_node(jid, node) | ||||
|         self.get_node(jid, node)['info'].add_feature( | ||||
|         new_node = self.add_node(jid, node) | ||||
|         new_node['info'].add_feature( | ||||
|                 data.get('feature', '')) | ||||
|  | ||||
|     def set_features(self, jid, node, ifrom, data): | ||||
| @@ -328,8 +329,8 @@ class StaticDisco(object): | ||||
|             features -- The new set of supported features. | ||||
|         """ | ||||
|         features = data.get('features', set()) | ||||
|         self.add_node(jid, node) | ||||
|         self.get_node(jid, node)['info']['features'] = features | ||||
|         new_node = self.add_node(jid, node) | ||||
|         new_node['info']['features'] = features | ||||
|  | ||||
|     def del_feature(self, jid, node, ifrom, data): | ||||
|         """ | ||||
| @@ -362,8 +363,8 @@ class StaticDisco(object): | ||||
|                      non-addressable items. | ||||
|             name  -- Optional human readable name for the item. | ||||
|         """ | ||||
|         self.add_node(jid, node) | ||||
|         self.get_node(jid, node)['items'].add_item( | ||||
|         new_node = self.add_node(jid, node) | ||||
|         new_node['items'].add_item( | ||||
|                 data.get('ijid', ''), | ||||
|                 node=data.get('inode', ''), | ||||
|                 name=data.get('name', '')) | ||||
| @@ -392,8 +393,8 @@ class StaticDisco(object): | ||||
|         if isinstance(data, Iq): | ||||
|             data = data['disco_info'] | ||||
|  | ||||
|         self.add_node(jid, node, ifrom) | ||||
|         self.get_node(jid, node, ifrom)['info'] = data | ||||
|         new_node = self.add_node(jid, node, ifrom) | ||||
|         new_node['info'] = data | ||||
|  | ||||
|     def get_cached_info(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|   | ||||
| @@ -261,7 +261,7 @@ class BinVal(ElementBase): | ||||
|  | ||||
|     def get_binval(self): | ||||
|         parent = self.parent() | ||||
|         xml = parent.find('{%s}BINVAL' % self.namespace) | ||||
|         xml = parent.xml.find('{%s}BINVAL' % self.namespace) | ||||
|         if xml is not None: | ||||
|             return base64.b64decode(bytes(xml.text)) | ||||
|         return b'' | ||||
|   | ||||
| @@ -19,23 +19,27 @@ from slixmpp.exceptions import XMPPError | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class ResultIterator(): | ||||
| class ResultIterator: | ||||
|  | ||||
|     """ | ||||
|     An iterator for Result Set Managment | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, query, interface, results='substanzas', amount=10, | ||||
|                        start=None, reverse=False): | ||||
|                        start=None, reverse=False, recv_interface=None, | ||||
|                        pre_cb=None, post_cb=None): | ||||
|         """ | ||||
|         Arguments: | ||||
|            query     -- The template query | ||||
|            interface -- The substanza of the query, for example disco_items | ||||
|            interface -- The substanza of the query to send, for example disco_items | ||||
|            recv_interface -- The substanza of the query to receive, for example disco_items | ||||
|            results   -- The query stanza's interface which provides a | ||||
|                         countable list of query results. | ||||
|            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 | ||||
|            pre_cb    -- Callback to run before sending the stanza | ||||
|            post_cb   -- Callback to run after receiving the reply | ||||
|  | ||||
|         Example: | ||||
|            q = Iq() | ||||
| @@ -49,17 +53,23 @@ class ResultIterator(): | ||||
|         self.amount = amount | ||||
|         self.start = start | ||||
|         self.interface = interface | ||||
|         if recv_interface: | ||||
|             self.recv_interface = recv_interface | ||||
|         else: | ||||
|             self.recv_interface = interface | ||||
|         self.pre_cb = pre_cb | ||||
|         self.post_cb = post_cb | ||||
|         self.results = results | ||||
|         self.reverse = reverse | ||||
|         self._stop = False | ||||
|  | ||||
|     def __iter__(self): | ||||
|     def __aiter__(self): | ||||
|         return self | ||||
|  | ||||
|     def __next__(self): | ||||
|         return self.next() | ||||
|     async def __anext__(self): | ||||
|         return await self.next() | ||||
|  | ||||
|     def next(self): | ||||
|     async def next(self): | ||||
|         """ | ||||
|         Return the next page of results from a query. | ||||
|  | ||||
| @@ -68,7 +78,7 @@ class ResultIterator(): | ||||
|               of items. | ||||
|         """ | ||||
|         if self._stop: | ||||
|             raise StopIteration | ||||
|             raise StopAsyncIteration | ||||
|         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) | ||||
| @@ -79,28 +89,32 @@ class ResultIterator(): | ||||
|             self.query[self.interface]['rsm']['after'] = self.start | ||||
|  | ||||
|         try: | ||||
|             r = self.query.send(block=True) | ||||
|             if self.pre_cb: | ||||
|                 self.pre_cb(self.query) | ||||
|             r = await self.query.send() | ||||
|  | ||||
|             if not r[self.interface]['rsm']['first'] and \ | ||||
|                not r[self.interface]['rsm']['last']: | ||||
|                 raise StopIteration | ||||
|             if not r[self.recv_interface]['rsm']['first'] and \ | ||||
|                not r[self.recv_interface]['rsm']['last']: | ||||
|                 raise StopAsyncIteration | ||||
|  | ||||
|             if r[self.interface]['rsm']['count'] and \ | ||||
|                r[self.interface]['rsm']['first_index']: | ||||
|                 count = int(r[self.interface]['rsm']['count']) | ||||
|                 first = int(r[self.interface]['rsm']['first_index']) | ||||
|                 num_items = len(r[self.interface][self.results]) | ||||
|             if r[self.recv_interface]['rsm']['count'] and \ | ||||
|                r[self.recv_interface]['rsm']['first_index']: | ||||
|                 count = int(r[self.recv_interface]['rsm']['count']) | ||||
|                 first = int(r[self.recv_interface]['rsm']['first_index']) | ||||
|                 num_items = len(r[self.recv_interface][self.results]) | ||||
|                 if first + num_items == count: | ||||
|                     self._stop = True | ||||
|  | ||||
|             if self.reverse: | ||||
|                 self.start = r[self.interface]['rsm']['first'] | ||||
|                 self.start = r[self.recv_interface]['rsm']['first'] | ||||
|             else: | ||||
|                 self.start = r[self.interface]['rsm']['last'] | ||||
|                 self.start = r[self.recv_interface]['rsm']['last'] | ||||
|  | ||||
|             if self.post_cb: | ||||
|                 self.post_cb(r) | ||||
|             return r | ||||
|         except XMPPError: | ||||
|             raise StopIteration | ||||
|             raise StopAsyncIteration | ||||
|  | ||||
|  | ||||
| class XEP_0059(BasePlugin): | ||||
| @@ -127,7 +141,8 @@ class XEP_0059(BasePlugin): | ||||
|     def session_bind(self, jid): | ||||
|         self.xmpp['xep_0030'].add_feature(Set.namespace) | ||||
|  | ||||
|     def iterate(self, stanza, interface, results='substanzas'): | ||||
|     def iterate(self, stanza, interface, results='substanzas', | ||||
|                 recv_interface=None, pre_cb=None, post_cb=None): | ||||
|         """ | ||||
|         Create a new result set iterator for a given stanza query. | ||||
|  | ||||
| @@ -137,9 +152,23 @@ class XEP_0059(BasePlugin): | ||||
|                          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. | ||||
|                          appended in the query stanza. For example, | ||||
|                          for disco#items queries the interface | ||||
|                          'disco_items' should be used. | ||||
|             recv_interface -- The name of the substanza from which the | ||||
|                               result set management stanza should be | ||||
|                               read in the result stanza. If unspecified, | ||||
|                               it will be set to the same value as the | ||||
|                               ``interface`` parameter. | ||||
|             pre_cb    -- Callback to run before sending each stanza e.g. | ||||
|                          setting the MAM queryid and starting a stanza | ||||
|                          collector. | ||||
|             post_cb   -- Callback to run after receiving each stanza e.g. | ||||
|                          stopping a MAM stanza collector in order to | ||||
|                          gather results. | ||||
|             results   -- The name of the interface containing the | ||||
|                          query results (typically just 'substanzas'). | ||||
|         """ | ||||
|         return ResultIterator(stanza, interface, results) | ||||
|         return ResultIterator(stanza, interface, results, | ||||
|                               recv_interface=recv_interface, pre_cb=pre_cb, | ||||
|                               post_cb=post_cb) | ||||
|   | ||||
| @@ -55,6 +55,7 @@ class XEP_0065(BasePlugin): | ||||
|         """Returns the socket associated to the SID.""" | ||||
|         return self._sessions.get(sid, None) | ||||
|  | ||||
|     @asyncio.coroutine | ||||
|     def handshake(self, to, ifrom=None, sid=None, timeout=None): | ||||
|         """ Starts the handshake to establish the socks5 bytestreams | ||||
|         connection. | ||||
| @@ -104,6 +105,7 @@ class XEP_0065(BasePlugin): | ||||
|             iq['socks'].add_streamhost(proxy, host, port) | ||||
|         return iq.send(timeout=timeout, callback=callback) | ||||
|  | ||||
|     @asyncio.coroutine | ||||
|     def discover_proxies(self, jid=None, ifrom=None, timeout=None): | ||||
|         """Auto-discover the JIDs of SOCKS5 proxies on an XMPP server.""" | ||||
|         if jid is None: | ||||
|   | ||||
| @@ -61,10 +61,12 @@ class XEP_0280(BasePlugin): | ||||
|         self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:carbons:2') | ||||
|  | ||||
|     def _handle_carbon_received(self, msg): | ||||
|         self.xmpp.event('carbon_received', msg) | ||||
|         if msg['from'].bare == self.xmpp.boundjid.bare: | ||||
|             self.xmpp.event('carbon_received', msg) | ||||
|  | ||||
|     def _handle_carbon_sent(self, msg): | ||||
|         self.xmpp.event('carbon_sent', msg) | ||||
|         if msg['from'].bare == self.xmpp.boundjid.bare: | ||||
|             self.xmpp.event('carbon_sent', msg) | ||||
|  | ||||
|     def enable(self, ifrom=None, timeout=None, callback=None, | ||||
|                timeout_callback=None): | ||||
|   | ||||
							
								
								
									
										16
									
								
								slixmpp/plugins/xep_0300/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								slixmpp/plugins/xep_0300/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2017 Emmanuel Gil Peyrot | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from slixmpp.plugins.xep_0300 import stanza | ||||
| from slixmpp.plugins.xep_0300.stanza import Hash | ||||
| from slixmpp.plugins.xep_0300.hash import XEP_0300 | ||||
|  | ||||
|  | ||||
| register_plugin(XEP_0300) | ||||
							
								
								
									
										87
									
								
								slixmpp/plugins/xep_0300/hash.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								slixmpp/plugins/xep_0300/hash.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2017 Emmanuel Gil Peyrot | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from base64 import b64encode | ||||
| import hashlib | ||||
| import logging | ||||
|  | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.plugins.xep_0300 import stanza, Hash | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class XEP_0300(BasePlugin): | ||||
|  | ||||
|     name = 'xep_0300' | ||||
|     description = 'XEP-0300: Use of Cryptographic Hash Functions in XMPP' | ||||
|     dependencies = {'xep_0030'} | ||||
|     stanza = stanza | ||||
|     default_config = { | ||||
|         'block_size': 1024 * 1024,  # One MiB | ||||
|         'prefered': 'sha-256', | ||||
|         'enable_sha-1': False, | ||||
|         'enable_sha-256': True, | ||||
|         'enable_sha-512': True, | ||||
|         'enable_sha3-256': True, | ||||
|         'enable_sha3-512': True, | ||||
|         'enable_BLAKE2b256': True, | ||||
|         'enable_BLAKE2b512': True, | ||||
|     } | ||||
|  | ||||
|     _hashlib_function = { | ||||
|         'sha-1': hashlib.sha1, | ||||
|         'sha-256': hashlib.sha256, | ||||
|         'sha-512': hashlib.sha512, | ||||
|         'sha3-256': lambda: hashlib.sha3_256(), | ||||
|         'sha3-512': lambda: hashlib.sha3_512(), | ||||
|         'BLAKE2b256': lambda: hashlib.blake2b(digest_size=32), | ||||
|         'BLAKE2b512': lambda: hashlib.blake2b(digest_size=64), | ||||
|     } | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         namespace = 'urn:xmpp:hash-function-text-names:%s' | ||||
|         self.enabled_hashes = [] | ||||
|         for algo in self._hashlib_function: | ||||
|             if getattr(self, 'enable_' + algo, False): | ||||
|                 # XXX: this is a hack for Python 3.5 or below, which | ||||
|                 # don’t support sha3 or blake2b… | ||||
|                 try: | ||||
|                     self._hashlib_function[algo]() | ||||
|                 except AttributeError: | ||||
|                     log.warn('Algorithm %s unavailable, disabling.', algo) | ||||
|                 else: | ||||
|                     self.enabled_hashes.append(namespace % algo) | ||||
|  | ||||
|     def session_bind(self, jid): | ||||
|         self.xmpp['xep_0030'].add_feature(Hash.namespace) | ||||
|  | ||||
|         for namespace in self.enabled_hashes: | ||||
|             self.xmpp['xep_0030'].add_feature(namespace) | ||||
|  | ||||
|     def plugin_end(self): | ||||
|         for namespace in self.enabled_hashes: | ||||
|             self.xmpp['xep_0030'].del_feature(namespace) | ||||
|  | ||||
|         self.xmpp['xep_0030'].del_feature(feature=Hash.namespace) | ||||
|  | ||||
|     def compute_hash(self, filename, function=None): | ||||
|         if function is None: | ||||
|             function = self.prefered | ||||
|         h = self._hashlib_function[function]() | ||||
|         with open(filename, 'rb') as f: | ||||
|             while True: | ||||
|                 block = f.read(self.block_size) | ||||
|                 if not block: | ||||
|                     break | ||||
|                 h.update(block) | ||||
|         hash_elem = Hash() | ||||
|         hash_elem['algo'] = function | ||||
|         hash_elem['value'] = b64encode(h.digest()) | ||||
|         return hash_elem | ||||
							
								
								
									
										35
									
								
								slixmpp/plugins/xep_0300/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								slixmpp/plugins/xep_0300/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2017 Emmanuel Gil Peyrot | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
|  | ||||
| class Hash(ElementBase): | ||||
|     name = 'hash' | ||||
|     namespace = 'urn:xmpp:hashes:2' | ||||
|     plugin_attrib = 'hash' | ||||
|     interfaces = {'algo', 'value'} | ||||
|  | ||||
|     allowed_algos = ['sha-1', 'sha-256', 'sha-512', 'sha3-256', 'sha3-512', 'BLAKE2b256', 'BLAKE2b512'] | ||||
|  | ||||
|     def set_algo(self, value): | ||||
|         if value in self.allowed_algos: | ||||
|             self._set_attr('algo', value) | ||||
|         elif value in [None, '']: | ||||
|             self._del_attr('algo') | ||||
|         else: | ||||
|             raise ValueError('Invalid algo: %s' % value) | ||||
|  | ||||
|     def get_value(self): | ||||
|         return self.xml.text | ||||
|  | ||||
|     def set_value(self, value): | ||||
|         self.xml.text = value | ||||
|  | ||||
|     def del_value(self): | ||||
|         self.xml.text = '' | ||||
| @@ -36,35 +36,58 @@ class XEP_0313(BasePlugin): | ||||
|         register_stanza_plugin(Iq, stanza.MAM) | ||||
|         register_stanza_plugin(Iq, stanza.Preferences) | ||||
|         register_stanza_plugin(Message, stanza.Result) | ||||
|         register_stanza_plugin(Message, stanza.Archived, iterable=True) | ||||
|         register_stanza_plugin(Iq, stanza.Fin) | ||||
|         register_stanza_plugin(stanza.Result, self.xmpp['xep_0297'].stanza.Forwarded) | ||||
|         register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set) | ||||
|         register_stanza_plugin(stanza.Fin, self.xmpp['xep_0059'].stanza.Set) | ||||
|  | ||||
|     def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None, | ||||
|                  timeout=None, callback=None, iterator=False): | ||||
|                  timeout=None, callback=None, iterator=False, rsm=None): | ||||
|         iq = self.xmpp.Iq() | ||||
|         query_id = iq['id'] | ||||
|  | ||||
|         iq['to'] = jid | ||||
|         iq['from'] = ifrom | ||||
|         iq['type'] = 'get' | ||||
|         iq['type'] = 'set' | ||||
|         iq['mam']['queryid'] = query_id | ||||
|         iq['mam']['start'] = start | ||||
|         iq['mam']['end'] = end | ||||
|         iq['mam']['with'] = with_jid | ||||
|         if rsm: | ||||
|             for key, value in rsm.items(): | ||||
|                 iq['mam']['rsm'][key] = str(value) | ||||
|  | ||||
|         cb_data = {} | ||||
|         def pre_cb(query): | ||||
|             query['mam']['queryid'] = query['id'] | ||||
|             collector = Collector( | ||||
|                 'MAM_Results_%s' % query_id, | ||||
|                 StanzaPath('message/mam_result@queryid=%s' % query['id'])) | ||||
|             self.xmpp.register_handler(collector) | ||||
|             cb_data['collector'] = collector | ||||
|  | ||||
|         def post_cb(result): | ||||
|             results = cb_data['collector'].stop() | ||||
|             if result['type'] == 'result': | ||||
|                 result['mam']['results'] = results | ||||
|  | ||||
|         if iterator: | ||||
|             return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results', | ||||
|                                                  recv_interface='mam_fin', | ||||
|                                                  pre_cb=pre_cb, post_cb=post_cb) | ||||
|  | ||||
|         collector = Collector( | ||||
|             'MAM_Results_%s' % query_id, | ||||
|             StanzaPath('message/mam_result@queryid=%s' % query_id)) | ||||
|         self.xmpp.register_handler(collector) | ||||
|  | ||||
|         if iterator: | ||||
|             return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results') | ||||
|         def wrapped_cb(iq): | ||||
|             results = collector.stop() | ||||
|             if iq['type'] == 'result': | ||||
|                 iq['mam']['results'] = results | ||||
|             callback(iq) | ||||
|             if callback: | ||||
|                 callback(iq) | ||||
|  | ||||
|         return iq.send(timeout=timeout, callback=wrapped_cb) | ||||
|  | ||||
|     def set_preferences(self, jid=None, default=None, always=None, never=None, | ||||
|   | ||||
| @@ -10,44 +10,76 @@ import datetime as dt | ||||
|  | ||||
| from slixmpp.jid import JID | ||||
| from slixmpp.xmlstream import ElementBase, ET | ||||
| from slixmpp.plugins import xep_0082 | ||||
| from slixmpp.plugins import xep_0082, xep_0004 | ||||
|  | ||||
|  | ||||
| class MAM(ElementBase): | ||||
|     name = 'query' | ||||
|     namespace = 'urn:xmpp:mam:tmp' | ||||
|     namespace = 'urn:xmpp:mam:2' | ||||
|     plugin_attrib = 'mam' | ||||
|     interfaces = {'queryid', 'start', 'end', 'with', 'results'} | ||||
|     sub_interfaces = {'start', 'end', 'with'} | ||||
|  | ||||
|     def setup(self, xml=None): | ||||
|         ElementBase.setup(self, xml) | ||||
|         self._form = xep_0004.stanza.Form() | ||||
|         self._form['type'] = 'submit' | ||||
|         field = self._form.add_field(var='FORM_TYPE', ftype='hidden', | ||||
|                              value='urn:xmpp:mam:2') | ||||
|         self.append(self._form) | ||||
|         self._results = [] | ||||
|  | ||||
|     def __get_fields(self): | ||||
|         return self._form.get_fields() | ||||
|  | ||||
|     def get_start(self): | ||||
|         timestamp = self._get_sub_text('start') | ||||
|         return xep_0082.parse(timestamp) | ||||
|         fields = self.__get_fields() | ||||
|         field = fields.get('start') | ||||
|         if field: | ||||
|             return xep_0082.parse(field['value']) | ||||
|  | ||||
|     def set_start(self, value): | ||||
|         if isinstance(value, dt.datetime): | ||||
|             value = xep_0082.format_datetime(value) | ||||
|         self._set_sub_text('start', value) | ||||
|         fields = self.__get_fields() | ||||
|         field = fields.get('start') | ||||
|         if field: | ||||
|             field['value'] = value | ||||
|         else: | ||||
|             field = self._form.add_field(var='start') | ||||
|             field['value'] = value | ||||
|  | ||||
|     def get_end(self): | ||||
|         timestamp = self._get_sub_text('end') | ||||
|         return xep_0082.parse(timestamp) | ||||
|         fields = self.__get_fields() | ||||
|         field = fields.get('end') | ||||
|         if field: | ||||
|             return xep_0082.parse(field['value']) | ||||
|  | ||||
|     def set_end(self, value): | ||||
|         if isinstance(value, dt.datetime): | ||||
|             value = xep_0082.format_datetime(value) | ||||
|         self._set_sub_text('end', value) | ||||
|         fields = self.__get_fields() | ||||
|         field = fields.get('end') | ||||
|         if field: | ||||
|             field['value'] = value | ||||
|         else: | ||||
|             field = self._form.add_field(var='end') | ||||
|             field['value'] = value | ||||
|  | ||||
|     def get_with(self): | ||||
|         return JID(self._get_sub_text('with')) | ||||
|         fields = self.__get_fields() | ||||
|         field = fields.get('with') | ||||
|         if field: | ||||
|             return JID(field['value']) | ||||
|  | ||||
|     def set_with(self, value): | ||||
|         self._set_sub_text('with', str(value)) | ||||
|  | ||||
|         fields = self.__get_fields() | ||||
|         field = fields.get('with') | ||||
|         if field: | ||||
|             field['with'] = str(value) | ||||
|         else: | ||||
|             field = self._form.add_field(var='with') | ||||
|             field['value'] = str(value) | ||||
|     # The results interface is meant only as an easy | ||||
|     # way to access the set of collected message responses | ||||
|     # from the query. | ||||
| @@ -64,7 +96,7 @@ class MAM(ElementBase): | ||||
|  | ||||
| class Preferences(ElementBase): | ||||
|     name = 'prefs' | ||||
|     namespace = 'urn:xmpp:mam:tmp' | ||||
|     namespace = 'urn:xmpp:mam:2' | ||||
|     plugin_attrib = 'mam_prefs' | ||||
|     interfaces = {'default', 'always', 'never'} | ||||
|     sub_interfaces = {'always', 'never'} | ||||
| @@ -118,22 +150,13 @@ class Preferences(ElementBase): | ||||
|             never.append(jid_xml) | ||||
|  | ||||
|  | ||||
| class Fin(ElementBase): | ||||
|     name = 'fin' | ||||
|     namespace = 'urn:xmpp:mam:2' | ||||
|     plugin_attrib = 'mam_fin' | ||||
|  | ||||
| class Result(ElementBase): | ||||
|     name = 'result' | ||||
|     namespace = 'urn:xmpp:mam:tmp' | ||||
|     namespace = 'urn:xmpp:mam:2' | ||||
|     plugin_attrib = 'mam_result' | ||||
|     interfaces = {'queryid', 'id'} | ||||
|  | ||||
|  | ||||
| class Archived(ElementBase): | ||||
|     name = 'archived' | ||||
|     namespace = 'urn:xmpp:mam:tmp' | ||||
|     plugin_attrib = 'mam_archived' | ||||
|     plugin_multi_attrib = 'mam_archives' | ||||
|     interfaces = {'by', 'id'} | ||||
|  | ||||
|     def get_by(self): | ||||
|         return JID(self._get_attr('by')) | ||||
|  | ||||
|     def set_by(self, value): | ||||
|         return self._set_attr('by', str(value)) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from datetime import datetime, timedelta | ||||
| from datetime import datetime, timedelta, timezone | ||||
|  | ||||
| from slixmpp.stanza import Presence | ||||
| from slixmpp.plugins import BasePlugin | ||||
| @@ -16,6 +16,10 @@ from slixmpp.xmlstream.matcher import StanzaPath | ||||
| from slixmpp.plugins.xep_0319 import stanza | ||||
|  | ||||
|  | ||||
| def get_local_timezone(): | ||||
|     return datetime.now(timezone.utc).astimezone().tzinfo | ||||
|  | ||||
|  | ||||
| class XEP_0319(BasePlugin): | ||||
|     name = 'xep_0319' | ||||
|     description = 'XEP-0319: Last User Interaction in Presence' | ||||
| @@ -47,10 +51,11 @@ class XEP_0319(BasePlugin): | ||||
|  | ||||
|     def idle(self, jid=None, since=None): | ||||
|         seconds = None | ||||
|         timezone = get_local_timezone() | ||||
|         if since is None: | ||||
|             since = datetime.now() | ||||
|             since = datetime.now(timezone) | ||||
|         else: | ||||
|             seconds = datetime.now() - since | ||||
|             seconds = datetime.now(timezone) - since | ||||
|         self.api['set_idle'](jid, None, None, since) | ||||
|         self.xmpp['xep_0012'].set_last_activity(jid=jid, seconds=seconds) | ||||
|  | ||||
|   | ||||
							
								
								
									
										15
									
								
								slixmpp/plugins/xep_0380/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								slixmpp/plugins/xep_0380/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from slixmpp.plugins.xep_0380.stanza import Encryption | ||||
| from slixmpp.plugins.xep_0380.eme import XEP_0380 | ||||
|  | ||||
|  | ||||
| register_plugin(XEP_0380) | ||||
							
								
								
									
										69
									
								
								slixmpp/plugins/xep_0380/eme.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								slixmpp/plugins/xep_0380/eme.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| import logging | ||||
|  | ||||
| import slixmpp | ||||
| from slixmpp.stanza import Message | ||||
| from slixmpp.xmlstream.handler import Callback | ||||
| from slixmpp.xmlstream.matcher import StanzaPath | ||||
| from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.plugins.xep_0380 import stanza, Encryption | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class XEP_0380(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     XEP-0380: Explicit Message Encryption | ||||
|     """ | ||||
|  | ||||
|     name = 'xep_0380' | ||||
|     description = 'XEP-0380: Explicit Message Encryption' | ||||
|     dependencies = {'xep_0030'} | ||||
|     default_config = { | ||||
|         'template': 'This message is encrypted with {name} ({namespace})', | ||||
|     } | ||||
|  | ||||
|     mechanisms = { | ||||
|         'jabber:x:encrypted': 'Legacy OpenPGP', | ||||
|         'urn:xmpp:ox:0': 'OpenPGP for XMPP', | ||||
|         'urn:xmpp:otr:0': 'OTR', | ||||
|         'eu.siacs.conversations.axolotl': 'Legacy OMEMO', | ||||
|         'urn:xmpp:omemo:0': 'OMEMO', | ||||
|     } | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         self.xmpp.register_handler( | ||||
|             Callback('Explicit Message Encryption', | ||||
|                      StanzaPath('message/eme'), | ||||
|                      self._handle_eme)) | ||||
|  | ||||
|         register_stanza_plugin(Message, Encryption) | ||||
|  | ||||
|     def plugin_end(self): | ||||
|         self.xmpp.remove_handler('Chat State') | ||||
|  | ||||
|     def session_bind(self, jid): | ||||
|         self.xmpp.plugin['xep_0030'].add_feature(Encryption.namespace) | ||||
|  | ||||
|     def has_eme(self, msg): | ||||
|         return msg.xml.find('{%s}encryption' % Encryption.namespace) is not None | ||||
|  | ||||
|     def replace_body_with_eme(self, msg): | ||||
|         eme = msg['eme'] | ||||
|         namespace = eme['namespace'] | ||||
|         name = self.mechanisms[namespace] if namespace in self.mechanisms else eme['name'] | ||||
|         body = self.config['template'].format(name=name, namespace=namespace) | ||||
|         msg['body'] = body | ||||
|  | ||||
|     def _handle_eme(self, msg): | ||||
|         self.xmpp.event('message_encryption', msg) | ||||
							
								
								
									
										16
									
								
								slixmpp/plugins/xep_0380/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								slixmpp/plugins/xep_0380/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
|  | ||||
| class Encryption(ElementBase): | ||||
|     name = 'encryption' | ||||
|     namespace = 'urn:xmpp:eme:0' | ||||
|     plugin_attrib = 'eme' | ||||
|     interfaces = {'namespace', 'name'} | ||||
							
								
								
									
										15
									
								
								slixmpp/plugins/xep_0394/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								slixmpp/plugins/xep_0394/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from slixmpp.plugins.xep_0394.stanza import Markup, Span, BlockCode, List, Li, BlockQuote | ||||
| from slixmpp.plugins.xep_0394.markup import XEP_0394 | ||||
|  | ||||
|  | ||||
| register_plugin(XEP_0394) | ||||
							
								
								
									
										161
									
								
								slixmpp/plugins/xep_0394/markup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								slixmpp/plugins/xep_0394/markup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
|  | ||||
| from slixmpp.stanza import Message | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.xmlstream import register_stanza_plugin, ET, tostring | ||||
| from slixmpp.plugins.xep_0394 import stanza, Markup, Span, BlockCode, List, Li, BlockQuote | ||||
| from slixmpp.plugins.xep_0071 import XHTML_IM | ||||
|  | ||||
|  | ||||
| class Start: | ||||
|     def __init__(self, elem): | ||||
|         self.elem = elem | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return 'Start(%s)' % self.elem | ||||
|  | ||||
|  | ||||
| class End: | ||||
|     def __init__(self, elem): | ||||
|         self.elem = elem | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return 'End(%s)' % self.elem | ||||
|  | ||||
|  | ||||
| class XEP_0394(BasePlugin): | ||||
|  | ||||
|     name = 'xep_0394' | ||||
|     description = 'XEP-0394: Message Markup' | ||||
|     dependencies = {'xep_0030', 'xep_0071'} | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         register_stanza_plugin(Message, Markup) | ||||
|  | ||||
|     def session_bind(self, jid): | ||||
|         self.xmpp['xep_0030'].add_feature(feature=Markup.namespace) | ||||
|  | ||||
|     def plugin_end(self): | ||||
|         self.xmpp['xep_0030'].del_feature(feature=Markup.namespace) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _split_first_level(body, markup_elem): | ||||
|         split_points = [] | ||||
|         elements = {} | ||||
|         for markup in markup_elem['substanzas']: | ||||
|             start = markup['start'] | ||||
|             end = markup['end'] | ||||
|             split_points.append(start) | ||||
|             split_points.append(end) | ||||
|             elements.setdefault(start, []).append(Start(markup)) | ||||
|             elements.setdefault(end, []).append(End(markup)) | ||||
|             if isinstance(markup, List): | ||||
|                 lis = markup['lis'] | ||||
|                 for i, li in enumerate(lis): | ||||
|                     start = li['start'] | ||||
|                     split_points.append(start) | ||||
|                     li_end = lis[i + 1]['start'] if i < len(lis) - 1 else end | ||||
|                     elements.setdefault(li_end, []).append(End(li)) | ||||
|                     elements.setdefault(start, []).append(Start(li)) | ||||
|         split_points = set(split_points) | ||||
|         new_body = [[]] | ||||
|         for i, letter in enumerate(body + '\x00'): | ||||
|             if i in split_points: | ||||
|                 body_elements = [] | ||||
|                 for elem in elements[i]: | ||||
|                     body_elements.append(elem) | ||||
|                 new_body.append(body_elements) | ||||
|                 new_body.append([]) | ||||
|             new_body[-1].append(letter) | ||||
|         new_body[-1] = new_body[-1][:-1] | ||||
|         final = [] | ||||
|         for chunk in new_body: | ||||
|             if not chunk: | ||||
|                 continue | ||||
|             final.append(''.join(chunk) if isinstance(chunk[0], str) else chunk) | ||||
|         return final | ||||
|  | ||||
|     def to_plain_text(self, body, markup_elem): | ||||
|         chunks = self._split_first_level(body, markup_elem) | ||||
|         final = [] | ||||
|         for chunk in chunks: | ||||
|             if isinstance(chunk, str): | ||||
|                 final.append(chunk) | ||||
|         return ''.join(final) | ||||
|  | ||||
|     def to_xhtml_im(self, body, markup_elem): | ||||
|         chunks = self._split_first_level(body, markup_elem) | ||||
|         final = [] | ||||
|         stack = [] | ||||
|         for chunk in chunks: | ||||
|             if isinstance(chunk, str): | ||||
|                 chunk = (chunk.replace("&", '&') | ||||
|                               .replace('<', '<') | ||||
|                               .replace('>', '>') | ||||
|                               .replace('"', '"') | ||||
|                               .replace("'", ''') | ||||
|                               .replace('\n', '<br/>')) | ||||
|                 final.append(chunk) | ||||
|                 continue | ||||
|             num_end = 0 | ||||
|             for elem in chunk: | ||||
|                 if isinstance(elem, End): | ||||
|                     num_end += 1 | ||||
|  | ||||
|             for i in range(num_end): | ||||
|                 stack_top = stack.pop() | ||||
|                 for elem in chunk: | ||||
|                     if not isinstance(elem, End): | ||||
|                         continue | ||||
|                     elem = elem.elem | ||||
|                     if elem is stack_top: | ||||
|                         if isinstance(elem, Span): | ||||
|                             final.append('</span>') | ||||
|                         elif isinstance(elem, BlockCode): | ||||
|                             final.append('</code></pre>') | ||||
|                         elif isinstance(elem, List): | ||||
|                             final.append('</ul>') | ||||
|                         elif isinstance(elem, Li): | ||||
|                             final.append('</li>') | ||||
|                         elif isinstance(elem, BlockQuote): | ||||
|                             final.append('</blockquote>') | ||||
|                         break | ||||
|                 else: | ||||
|                     assert False | ||||
|             for elem in chunk: | ||||
|                 if not isinstance(elem, Start): | ||||
|                     continue | ||||
|                 elem = elem.elem | ||||
|                 stack.append(elem) | ||||
|                 if isinstance(elem, Span): | ||||
|                     style = [] | ||||
|                     for type_ in elem['types']: | ||||
|                         if type_ == 'emphasis': | ||||
|                             style.append('font-style: italic;') | ||||
|                         if type_ == 'code': | ||||
|                             style.append('font-family: monospace;') | ||||
|                         if type_ == 'deleted': | ||||
|                             style.append('text-decoration: line-through;') | ||||
|                     final.append("<span style='%s'>" % ' '.join(style)) | ||||
|                 elif isinstance(elem, BlockCode): | ||||
|                     final.append('<pre><code>') | ||||
|                 elif isinstance(elem, List): | ||||
|                     final.append('<ul>') | ||||
|                 elif isinstance(elem, Li): | ||||
|                     final.append('<li>') | ||||
|                 elif isinstance(elem, BlockQuote): | ||||
|                     final.append('<blockquote>') | ||||
|         p = "<p xmlns='http://www.w3.org/1999/xhtml'>%s</p>" % ''.join(final) | ||||
|         p2 = ET.fromstring(p) | ||||
|         print('coucou', p, tostring(p2)) | ||||
|         xhtml_im = XHTML_IM() | ||||
|         xhtml_im['body'] = p2 | ||||
|         return xhtml_im | ||||
							
								
								
									
										123
									
								
								slixmpp/plugins/xep_0394/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								slixmpp/plugins/xep_0394/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from slixmpp.xmlstream import ElementBase, register_stanza_plugin, ET | ||||
|  | ||||
|  | ||||
| class Markup(ElementBase): | ||||
|     namespace = 'urn:xmpp:markup:0' | ||||
|     name = 'markup' | ||||
|     plugin_attrib = 'markup' | ||||
|  | ||||
|  | ||||
| class _FirstLevel(ElementBase): | ||||
|     namespace = 'urn:xmpp:markup:0' | ||||
|     interfaces = {'start', 'end'} | ||||
|  | ||||
|     def get_start(self): | ||||
|         return int(self._get_attr('start')) | ||||
|  | ||||
|     def set_start(self, value): | ||||
|         self._set_attr('start', '%d' % value) | ||||
|  | ||||
|     def get_end(self): | ||||
|         return int(self._get_attr('end')) | ||||
|  | ||||
|     def set_end(self, value): | ||||
|         self._set_attr('end', '%d' % value) | ||||
|  | ||||
| class Span(_FirstLevel): | ||||
|     name = 'span' | ||||
|     plugin_attrib = 'span' | ||||
|     plugin_multi_attrib = 'spans' | ||||
|     interfaces = {'start', 'end', 'types'} | ||||
|  | ||||
|     def get_types(self): | ||||
|         types = [] | ||||
|         if self.xml.find('{urn:xmpp:markup:0}emphasis') is not None: | ||||
|             types.append('emphasis') | ||||
|         if self.xml.find('{urn:xmpp:markup:0}code') is not None: | ||||
|             types.append('code') | ||||
|         if self.xml.find('{urn:xmpp:markup:0}deleted') is not None: | ||||
|             types.append('deleted') | ||||
|         return types | ||||
|  | ||||
|     def set_types(self, value): | ||||
|         del self['types'] | ||||
|         for type_ in value: | ||||
|             if type_ == 'emphasis': | ||||
|                 self.xml.append(ET.Element('{urn:xmpp:markup:0}emphasis')) | ||||
|             elif type_ == 'code': | ||||
|                 self.xml.append(ET.Element('{urn:xmpp:markup:0}code')) | ||||
|             elif type_ == 'deleted': | ||||
|                 self.xml.append(ET.Element('{urn:xmpp:markup:0}deleted')) | ||||
|  | ||||
|     def det_types(self): | ||||
|         for child in self.xml: | ||||
|             self.xml.remove(child) | ||||
|  | ||||
|  | ||||
| class _SpanType(ElementBase): | ||||
|     namespace = 'urn:xmpp:markup:0' | ||||
|  | ||||
|  | ||||
| class EmphasisType(_SpanType): | ||||
|     name = 'emphasis' | ||||
|     plugin_attrib = 'emphasis' | ||||
|  | ||||
|  | ||||
| class CodeType(_SpanType): | ||||
|     name = 'code' | ||||
|     plugin_attrib = 'code' | ||||
|  | ||||
|  | ||||
| class DeletedType(_SpanType): | ||||
|     name = 'deleted' | ||||
|     plugin_attrib = 'deleted' | ||||
|  | ||||
|  | ||||
| class BlockCode(_FirstLevel): | ||||
|     name = 'bcode' | ||||
|     plugin_attrib = 'bcode' | ||||
|     plugin_multi_attrib = 'bcodes' | ||||
|  | ||||
|  | ||||
| class List(_FirstLevel): | ||||
|     name = 'list' | ||||
|     plugin_attrib = 'list' | ||||
|     plugin_multi_attrib = 'lists' | ||||
|     interfaces = {'start', 'end', 'li'} | ||||
|  | ||||
|  | ||||
| class Li(ElementBase): | ||||
|     namespace = 'urn:xmpp:markup:0' | ||||
|     name = 'li' | ||||
|     plugin_attrib = 'li' | ||||
|     plugin_multi_attrib = 'lis' | ||||
|     interfaces = {'start'} | ||||
|  | ||||
|     def get_start(self): | ||||
|         return int(self._get_attr('start')) | ||||
|  | ||||
|     def set_start(self, value): | ||||
|         self._set_attr('start', '%d' % value) | ||||
|  | ||||
|  | ||||
| class BlockQuote(_FirstLevel): | ||||
|     name = 'bquote' | ||||
|     plugin_attrib = 'bquote' | ||||
|     plugin_multi_attrib = 'bquotes' | ||||
|  | ||||
| register_stanza_plugin(Markup, Span, iterable=True) | ||||
| register_stanza_plugin(Markup, BlockCode, iterable=True) | ||||
| register_stanza_plugin(Markup, List, iterable=True) | ||||
| register_stanza_plugin(Markup, BlockQuote, iterable=True) | ||||
| register_stanza_plugin(Span, EmphasisType) | ||||
| register_stanza_plugin(Span, CodeType) | ||||
| register_stanza_plugin(Span, DeletedType) | ||||
| register_stanza_plugin(List, Li, iterable=True) | ||||
| @@ -291,8 +291,7 @@ class SCRAM(Mech): | ||||
|         cbind_input = self.gs2_header + cbind_data | ||||
|         channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'') | ||||
|  | ||||
|         client_final_message_without_proof = channel_binding + b',' + \ | ||||
|                                              b'r=' + nonce | ||||
|         client_final_message_without_proof = channel_binding + b',r=' + nonce | ||||
|  | ||||
|         salted_password = self.Hi(self.credentials['password'], | ||||
|                                        salt, | ||||
|   | ||||
| @@ -9,5 +9,5 @@ | ||||
| # We don't want to have to import the entire library | ||||
| # just to get the version info for setup.py | ||||
|  | ||||
| __version__ = '1.2.2' | ||||
| __version_info__ = (1, 2, 2) | ||||
| __version__ = '1.3.0' | ||||
| __version_info__ = (1, 3, 0) | ||||
|   | ||||
| @@ -19,10 +19,10 @@ import ssl | ||||
| import weakref | ||||
| import uuid | ||||
|  | ||||
| import xml.etree.ElementTree | ||||
| import xml.etree.ElementTree as ET | ||||
|  | ||||
| from slixmpp.xmlstream.asyncio import asyncio | ||||
| from slixmpp.xmlstream import tostring, highlight | ||||
| from slixmpp.xmlstream import tostring | ||||
| from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase | ||||
| from slixmpp.xmlstream.resolver import resolve, default_resolver | ||||
|  | ||||
| @@ -204,6 +204,9 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         #: We use an ID prefix to ensure that all ID values are unique. | ||||
|         self._id_prefix = '%s-' % uuid.uuid4() | ||||
|  | ||||
|         # Current connection attempt (Future) | ||||
|         self._current_connection_attempt = None | ||||
|  | ||||
|         #: A list of DNS results that have not yet been tried. | ||||
|         self.dns_answers = None | ||||
|  | ||||
| @@ -265,6 +268,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|                                localhost | ||||
|  | ||||
|         """ | ||||
|         self.cancel_connection_attempt() | ||||
|         if host and port: | ||||
|             self.address = (host, int(port)) | ||||
|         try: | ||||
| @@ -281,7 +285,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|             self.disable_starttls = disable_starttls | ||||
|  | ||||
|         self.event("connecting") | ||||
|         asyncio.async(self._connect_routine()) | ||||
|         self._current_connection_attempt = asyncio.async(self._connect_routine()) | ||||
|  | ||||
|     @asyncio.coroutine | ||||
|     def _connect_routine(self): | ||||
| @@ -289,7 +293,8 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|  | ||||
|         record = yield from self.pick_dns_answer(self.default_domain) | ||||
|         if record is not None: | ||||
|             host, address, port = record | ||||
|             host, address, dns_port = record | ||||
|             port = dns_port if dns_port else self.address[1] | ||||
|             self.address = (address, port) | ||||
|             self._service_name = host | ||||
|         else: | ||||
| @@ -297,13 +302,19 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|             # and try (host, port) as a last resort | ||||
|             self.dns_answers = None | ||||
|  | ||||
|         if self.use_ssl: | ||||
|             ssl_context = self.get_ssl_context() | ||||
|         else: | ||||
|             ssl_context = None | ||||
|  | ||||
|         yield from asyncio.sleep(self.connect_loop_wait) | ||||
|         try: | ||||
|             yield from self.loop.create_connection(lambda: self, | ||||
|                                                    self.address[0], | ||||
|                                                    self.address[1], | ||||
|                                                    ssl=self.use_ssl, | ||||
|                                                    ssl=ssl_context, | ||||
|                                                    server_hostname=self.default_domain if self.use_ssl else None) | ||||
|             self.connect_loop_wait = 0 | ||||
|         except Socket.gaierror as e: | ||||
|             self.event('connection_failed', | ||||
|                        'No DNS record available for %s' % self.default_domain) | ||||
| @@ -311,9 +322,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|             log.debug('Connection failed: %s', e) | ||||
|             self.event("connection_failed", e) | ||||
|             self.connect_loop_wait = self.connect_loop_wait * 2 + 1 | ||||
|             asyncio.async(self._connect_routine()) | ||||
|         else: | ||||
|             self.connect_loop_wait = 0 | ||||
|             self._current_connection_attempt = asyncio.async(self._connect_routine()) | ||||
|  | ||||
|     def process(self, *, forever=True, timeout=None): | ||||
|         """Process all the available XMPP events (receiving or sending data on the | ||||
| @@ -339,7 +348,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         """ | ||||
|         self.xml_depth = 0 | ||||
|         self.xml_root = None | ||||
|         self.parser = xml.etree.ElementTree.XMLPullParser(("start", "end")) | ||||
|         self.parser = ET.XMLPullParser(("start", "end")) | ||||
|  | ||||
|     def connection_made(self, transport): | ||||
|         """Called when the TCP connection has been established with the server | ||||
| @@ -358,33 +367,50 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         event.  This could trigger one or more event (a stanza is received, | ||||
|         the stream is opened, etc). | ||||
|         """ | ||||
|         if self.parser is None: | ||||
|             log.warning('Received data before the connection is established: %r', | ||||
|                         data) | ||||
|             return | ||||
|         self.parser.feed(data) | ||||
|         for event, xml in self.parser.read_events(): | ||||
|             if event == 'start': | ||||
|                 if self.xml_depth == 0: | ||||
|                     # We have received the start of the root element. | ||||
|                     self.xml_root = xml | ||||
|                     log.debug('[33;1mRECV[0m: %s', highlight(tostring(self.xml_root, xmlns=self.default_ns, | ||||
|                                                          stream=self, | ||||
|                                                          top_level=True, | ||||
|                                                          open_only=True))) | ||||
|                     self.start_stream_handler(self.xml_root) | ||||
|                 self.xml_depth += 1 | ||||
|             if event == 'end': | ||||
|                 self.xml_depth -= 1 | ||||
|                 if self.xml_depth == 0: | ||||
|                     # The stream's root element has closed, | ||||
|                     # terminating the stream. | ||||
|                     log.debug("End of stream received") | ||||
|                     self.abort() | ||||
|                 elif self.xml_depth == 1: | ||||
|                     # A stanza is an XML element that is a direct child of | ||||
|                     # the root element, hence the check of depth == 1 | ||||
|                     self._spawn_event(xml) | ||||
|                     if self.xml_root is not None: | ||||
|                         # Keep the root element empty of children to | ||||
|                         # save on memory use. | ||||
|                         self.xml_root.clear() | ||||
|         try: | ||||
|             for event, xml in self.parser.read_events(): | ||||
|                 if event == 'start': | ||||
|                     if self.xml_depth == 0: | ||||
|                         # We have received the start of the root element. | ||||
|                         self.xml_root = xml | ||||
|                         log.debug('RECV: %s', tostring(self.xml_root, | ||||
|                                                        xmlns=self.default_ns, | ||||
|                                                        stream=self, | ||||
|                                                        top_level=True, | ||||
|                                                        open_only=True)) | ||||
|                         self.start_stream_handler(self.xml_root) | ||||
|                     self.xml_depth += 1 | ||||
|                 if event == 'end': | ||||
|                     self.xml_depth -= 1 | ||||
|                     if self.xml_depth == 0: | ||||
|                         # The stream's root element has closed, | ||||
|                         # terminating the stream. | ||||
|                         log.debug("End of stream received") | ||||
|                         self.abort() | ||||
|                     elif self.xml_depth == 1: | ||||
|                         # A stanza is an XML element that is a direct child of | ||||
|                         # the root element, hence the check of depth == 1 | ||||
|                         self._spawn_event(xml) | ||||
|                         if self.xml_root is not None: | ||||
|                             # Keep the root element empty of children to | ||||
|                             # save on memory use. | ||||
|                             self.xml_root.clear() | ||||
|         except ET.ParseError: | ||||
|             log.error('Parse error: %r', data) | ||||
|  | ||||
|             # Due to cyclic dependencies, this can’t be imported at the module | ||||
|             # level. | ||||
|             from slixmpp.stanza.stream_error import StreamError | ||||
|             error = StreamError() | ||||
|             error['condition'] = 'not-well-formed' | ||||
|             error['text'] = 'Server sent: %r' % data | ||||
|             self.send(error) | ||||
|             self.disconnect() | ||||
|  | ||||
|     def is_connected(self): | ||||
|         return self.transport is not None | ||||
| @@ -408,6 +434,17 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         self.transport = None | ||||
|         self.socket = None | ||||
|  | ||||
|     def cancel_connection_attempt(self): | ||||
|         """ | ||||
|         Immediatly cancel the current create_connection() Future. | ||||
|         This is useful when a client using slixmpp tries to connect | ||||
|         on flaky networks, where sometimes a connection just gets lost | ||||
|         and it needs to reconnect while the attempt is still ongoing. | ||||
|         """ | ||||
|         if self._current_connection_attempt: | ||||
|             self._current_connection_attempt.cancel() | ||||
|             self._current_connection_attempt = None | ||||
|  | ||||
|     def disconnect(self, wait=2.0): | ||||
|         """Close the XML stream and wait for an acknowldgement from the server for | ||||
|         at most `wait` seconds.  After the given number of seconds has | ||||
| @@ -421,6 +458,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         :param wait: Time to wait for a response from the server. | ||||
|  | ||||
|         """ | ||||
|         self.cancel_connection_attempt() | ||||
|         if self.transport: | ||||
|             self.send_raw(self.stream_footer) | ||||
|             self.schedule('Disconnect wait', wait, | ||||
| @@ -430,6 +468,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         """ | ||||
|         Forcibly close the connection | ||||
|         """ | ||||
|         self.cancel_connection_attempt() | ||||
|         if self.transport: | ||||
|             self.transport.close() | ||||
|             self.transport.abort() | ||||
| @@ -469,14 +508,10 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         """ | ||||
|         pass | ||||
|  | ||||
|     def start_tls(self): | ||||
|         """Perform handshakes for TLS. | ||||
|  | ||||
|         If the handshake is successful, the XML stream will need | ||||
|         to be restarted. | ||||
|     def get_ssl_context(self): | ||||
|         """ | ||||
|         Get SSL context. | ||||
|         """ | ||||
|         self.event_when_connected = "tls_success" | ||||
|  | ||||
|         if self.ciphers is not None: | ||||
|             self.ssl_context.set_ciphers(self.ciphers) | ||||
|         if self.keyfile and self.certfile: | ||||
| @@ -491,7 +526,18 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|             self.ssl_context.verify_mode = ssl.CERT_REQUIRED | ||||
|             self.ssl_context.load_verify_locations(cafile=self.ca_certs) | ||||
|  | ||||
|         ssl_connect_routine = self.loop.create_connection(lambda: self, ssl=self.ssl_context, | ||||
|         return self.ssl_context | ||||
|  | ||||
|     def start_tls(self): | ||||
|         """Perform handshakes for TLS. | ||||
|  | ||||
|         If the handshake is successful, the XML stream will need | ||||
|         to be restarted. | ||||
|         """ | ||||
|         self.event_when_connected = "tls_success" | ||||
|  | ||||
|         ssl_context = self.get_ssl_context() | ||||
|         ssl_connect_routine = self.loop.create_connection(lambda: self, ssl=ssl_context, | ||||
|                                                           sock=self.socket, | ||||
|                                                           server_hostname=self.default_domain) | ||||
|         @asyncio.coroutine | ||||
| @@ -506,7 +552,9 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|                 else: | ||||
|                     self.event('ssl_invalid_chain', e) | ||||
|             else: | ||||
|                 der_cert = transp.get_extra_info("socket").getpeercert(True) | ||||
|                 # Workaround for a regression in 3.4 where ssl_object was not set. | ||||
|                 der_cert = transp.get_extra_info("ssl_object", | ||||
|                                                  default=transp.get_extra_info("socket")).getpeercert(True) | ||||
|                 pem_cert = ssl.DER_cert_to_PEM_cert(der_cert) | ||||
|                 self.event('ssl_cert', pem_cert) | ||||
|  | ||||
| @@ -842,8 +890,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|                     if data is None: | ||||
|                         return | ||||
|             str_data = tostring(data.xml, xmlns=self.default_ns, | ||||
|                                           stream=self, | ||||
|                                           top_level=True) | ||||
|                                 stream=self, top_level=True) | ||||
|             self.send_raw(str_data) | ||||
|         else: | ||||
|             self.send_raw(data) | ||||
| @@ -861,7 +908,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|  | ||||
|         :param string data: Any bytes or utf-8 string value. | ||||
|         """ | ||||
|         log.debug("[36;1mSEND[0m: %s", highlight(data)) | ||||
|         log.debug("SEND: %s", data) | ||||
|         if not self.transport: | ||||
|             raise NotConnectedError() | ||||
|         if isinstance(data, str): | ||||
| @@ -914,7 +961,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         if stanza is None: | ||||
|             return | ||||
|  | ||||
|         log.debug("[33;1mRECV[0m: %s", highlight(stanza)) | ||||
|         log.debug("RECV: %s", stanza) | ||||
|  | ||||
|         # Match the stanza against registered handlers. Handlers marked | ||||
|         # to run "in stream" will be executed immediately; the rest will | ||||
|   | ||||
							
								
								
									
										57
									
								
								tests/test_stanza_xep_0300.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								tests/test_stanza_xep_0300.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| """ | ||||
|     Slixmpp: The Slick XMPP Library | ||||
|     Copyright (C) 2017 Emmanuel Gil Peyrot | ||||
|     This file is part of Slixmpp. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| import unittest | ||||
| from slixmpp import Iq | ||||
| from slixmpp.test import SlixTest | ||||
| from slixmpp.plugins.xep_0300 import Hash | ||||
| from slixmpp.xmlstream import register_stanza_plugin | ||||
|  | ||||
|  | ||||
| class TestHash(SlixTest): | ||||
|  | ||||
|     def setUp(self): | ||||
|         register_stanza_plugin(Iq, Hash) | ||||
|  | ||||
|     def testSimpleElement(self): | ||||
|         """Test that the element is created correctly.""" | ||||
|         iq = Iq() | ||||
|         iq['type'] = 'set' | ||||
|         iq['hash']['algo'] = 'sha-256' | ||||
|         iq['hash']['value'] = 'EQgS9n+h4fARf289cCQcGkKnsHcRqTwkd8xRbZBC+ds=' | ||||
|  | ||||
|         self.check(iq, """ | ||||
|           <iq type="set"> | ||||
|             <hash xmlns="urn:xmpp:hashes:2" algo="sha-256">EQgS9n+h4fARf289cCQcGkKnsHcRqTwkd8xRbZBC+ds=</hash> | ||||
|           </iq> | ||||
|         """) | ||||
|  | ||||
|     def testInvalidAlgo(self): | ||||
|         """Test that invalid algos raise an exception.""" | ||||
|         iq = Iq() | ||||
|         iq['type'] = 'set' | ||||
|         try: | ||||
|             iq['hash']['algo'] = 'coucou' | ||||
|         except ValueError: | ||||
|             pass | ||||
|         else: | ||||
|             raise self.failureException | ||||
|  | ||||
|     #def testDisabledAlgo(self): | ||||
|     #    """Test that disabled algos aren’t used.""" | ||||
|     #    iq = Iq() | ||||
|     #    iq['type'] = 'set' | ||||
|     #    try: | ||||
|     #        iq['hash']['algo'] = 'sha-1' | ||||
|     #    except ValueError: | ||||
|     #        pass | ||||
|     #    else: | ||||
|     #        raise self.failureException | ||||
|  | ||||
|  | ||||
| suite = unittest.TestLoader().loadTestsFromTestCase(TestHash) | ||||
							
								
								
									
										37
									
								
								tests/test_stanza_xep_0380.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								tests/test_stanza_xep_0380.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import unittest | ||||
| from slixmpp import Message | ||||
| from slixmpp.test import SlixTest | ||||
| import slixmpp.plugins.xep_0380 as xep_0380 | ||||
| from slixmpp.xmlstream import register_stanza_plugin | ||||
|  | ||||
|  | ||||
| class TestEME(SlixTest): | ||||
|  | ||||
|     def setUp(self): | ||||
|         register_stanza_plugin(Message, xep_0380.stanza.Encryption) | ||||
|  | ||||
|     def testCreateEME(self): | ||||
|         """Testing creating EME.""" | ||||
|  | ||||
|         xmlstring = """ | ||||
|           <message> | ||||
|             <encryption xmlns="urn:xmpp:eme:0" namespace="%s"%s /> | ||||
|           </message> | ||||
|         """ | ||||
|  | ||||
|         msg = self.Message() | ||||
|         self.check(msg, "<message />") | ||||
|  | ||||
|         msg['eme']['namespace'] = 'urn:xmpp:otr:0' | ||||
|         self.check(msg, xmlstring % ('urn:xmpp:otr:0', '')) | ||||
|  | ||||
|         msg['eme']['namespace'] = 'urn:xmpp:openpgp:0' | ||||
|         self.check(msg, xmlstring % ('urn:xmpp:openpgp:0', '')) | ||||
|  | ||||
|         msg['eme']['name'] = 'OX' | ||||
|         self.check(msg, xmlstring % ('urn:xmpp:openpgp:0', ' name="OX"')) | ||||
|  | ||||
|         del msg['eme'] | ||||
|         self.check(msg, "<message />") | ||||
|  | ||||
| suite = unittest.TestLoader().loadTestsFromTestCase(TestEME) | ||||
		Reference in New Issue
	
	Block a user