diff --git a/examples/proxy_echo_client.py b/examples/proxy_echo_client.py new file mode 100755 index 0000000..4db9a55 --- /dev/null +++ b/examples/proxy_echo_client.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import sys +import logging +import time +import getpass +from optparse import OptionParser + +import sleekxmpp + +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. +if sys.version_info < (3, 0): + reload(sys) + sys.setdefaultencoding('utf8') + + +class EchoBot(sleekxmpp.ClientXMPP): + + """ + A simple SleekXMPP bot that will echo messages it + receives, along with a short thank you message. + """ + + def __init__(self, jid, password): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + + # The session_start event will be triggered when + # the bot establishes its connection with the server + # and the XML streams are ready for use. We want to + # listen for this event so that we we can intialize + # our roster. + self.add_event_handler("session_start", self.start) + + # 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 intial + 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. + """ + msg.reply("Thanks for sending\n%(body)s" % msg).send() + + +if __name__ == '__main__': + # Setup the command line arguments. + optp = OptionParser() + + # Output verbosity options. + optp.add_option('-q', '--quiet', help='set logging to ERROR', + action='store_const', dest='loglevel', + const=logging.ERROR, default=logging.INFO) + optp.add_option('-d', '--debug', help='set logging to DEBUG', + action='store_const', dest='loglevel', + const=logging.DEBUG, default=logging.INFO) + optp.add_option('-v', '--verbose', help='set logging to COMM', + action='store_const', dest='loglevel', + const=5, default=logging.INFO) + + # JID and password options. + optp.add_option("-j", "--jid", dest="jid", + help="JID to use") + optp.add_option("-p", "--password", dest="password", + help="password to use") + optp.add_option("--phost", dest="proxy_host", + help="Proxy hostname") + optp.add_option("--pport", dest="proxy_port", + help="Proxy port") + optp.add_option("--puser", dest="proxy_user", + help="Proxy username") + optp.add_option("--ppass", dest="proxy_pass", + help="Proxy password") + + + + opts, args = optp.parse_args() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + if opts.jid is None: + opts.jid = raw_input("Username: ") + if opts.password is None: + opts.password = getpass.getpass("Password: ") + if opts.proxy_host is None: + opts.proxy_host = raw_input("Proxy host: ") + if opts.proxy_port is None: + opts.proxy_port = raw_input("Proxy port: ") + if opts.proxy_user is None: + opts.proxy_user = raw_input("Proxy username: ") + if opts.proxy_pass is None and opts.proxy_user: + opts.proxy_pass = getpass.getpass("Proxy password: ") + + # Setup the EchoBot and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = EchoBot(opts.jid, opts.password) + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0004') # Data Forms + xmpp.register_plugin('xep_0060') # PubSub + xmpp.register_plugin('xep_0199') # XMPP Ping + + # If you are working with an OpenFire server, you may need + # to adjust the SSL version used: + # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 + + # If you want to verify the SSL certificates offered by a server: + # xmpp.ca_certs = "path/to/ca/cert" + + xmpp.use_proxy = True + xmpp.proxy_config = { + 'host': opts.proxy_host, + 'port': int(opts.proxy_port), + 'username': opts.proxy_user, + 'password': opts.proxy_pass} + + # Connect to the XMPP server and start processing XMPP stanzas. + if xmpp.connect(): + # If you do not have the pydns library installed, you will need + # to manually specify the name of the server if it does not match + # the one in the JID. For example, to use Google Talk you would + # need to use: + # + # if xmpp.connect(('talk.google.com', 5222)): + # ... + xmpp.process(threaded=False) + print("Done") + else: + print("Unable to connect.") diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index 5ad1174..a53cfb0 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -15,5 +15,5 @@ from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET -__version__ = '1.0beta5' -__version_info__ = (1, 0, 0, 'beta5', 0) +__version__ = '1.0beta6' +__version_info__ = (1, 0, 0, 'beta6', 0) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 02d4764..0328e39 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -168,18 +168,23 @@ class ClientXMPP(BaseXMPP): addresses = {} intmax = 0 + topprio = 65535 for answer in answers: - intmax += answer.priority - addresses[intmax] = (answer.target.to_text()[:-1], + topprio = min(topprio, answer.priority) + for answer in answers: + if answer.priority == topprio: + intmax += answer.weight + addresses[intmax] = (answer.target.to_text()[:-1], answer.port) + #python3 returns a generator for dictionary keys - priorities = [x for x in addresses.keys()] - priorities.sort() + items = [x for x in addresses.keys()] + items.sort() picked = random.randint(0, intmax) - for priority in priorities: - if picked <= priority: - address = addresses[priority] + for item in items: + if picked <= item: + address = addresses[item] break if not address: diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index d27937a..b48a4c0 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -6,5 +6,6 @@ See the file LICENSE for copying permission. """ __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', - 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086', - 'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify'] + 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082', + 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199', + 'xep_0202', 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify'] diff --git a/sleekxmpp/plugins/xep_0066/oob.py b/sleekxmpp/plugins/xep_0066/oob.py index b432235..98cb81c 100644 --- a/sleekxmpp/plugins/xep_0066/oob.py +++ b/sleekxmpp/plugins/xep_0066/oob.py @@ -6,8 +6,10 @@ See the file LICENSE for copying permission. """ +import logging from sleekxmpp.stanza import Message, Presence, Iq +from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath @@ -15,6 +17,9 @@ from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.xep_0066 import stanza +log = logging.getLogger(__name__) + + class xep_0066(base_plugin): """ @@ -43,6 +48,9 @@ class xep_0066(base_plugin): self.description = 'Out-of-Band Transfer' self.stanza = stanza + self.url_handlers = {'global': self._default_handler, + 'jid': {}} + register_stanza_plugin(Iq, stanza.OOBTransfer) register_stanza_plugin(Message, stanza.OOB) register_stanza_plugin(Presence, stanza.OOB) @@ -58,6 +66,28 @@ class xep_0066(base_plugin): self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace) self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace) + def register_url_handler(self, jid=None, handler=None): + """ + Register a handler to process download requests, either for all + JIDs or a single JID. + + Arguments: + jid -- If None, then set the handler as a global default. + handler -- If None, then remove the existing handler for the + given JID, or reset the global handler if the JID + is None. + """ + if jid is None: + if handler is not None: + self.url_handlers['global'] = handler + else: + self.url_handlers['global'] = self._default_handler + else: + if handler is not None: + self.url_handlers['jid'][jid] = handler + else: + del self.url_handlers['jid'][jid] + def send_oob(self, to, url, desc=None, ifrom=None, **iqargs): """ Initiate a basic file transfer by sending the URL of @@ -84,6 +114,41 @@ class xep_0066(base_plugin): iq['oob_transfer']['desc'] = desc return iq.send(**iqargs) + def _run_url_handler(self, iq): + """ + Execute the appropriate handler for a transfer request. + + Arguments: + iq -- The Iq stanza containing the OOB transfer request. + """ + if iq['to'] in self.url_handlers['jid']: + return self.url_handlers['jid'][jid](iq) + else: + if self.url_handlers['global']: + self.url_handlers['global'](iq) + else: + raise XMPPError('service-unavailable') + + def _default_handler(self, iq): + """ + As a safe default, don't actually download files. + + Register a new handler using self.register_url_handler to + screen requests and download files. + + Arguments: + iq -- The Iq stanza containing the OOB transfer request. + """ + raise XMPPError('service-unavailable') + def _handle_transfer(self, iq): - """Handle receiving an out-of-band transfer request.""" - self.xmpp.event('oob_transfer', iq) + """ + Handle receiving an out-of-band transfer request. + + Arguments: + iq -- An Iq stanza containing an OOB transfer request. + """ + log.debug('Received out-of-band data request for %s from %s:' % ( + iq['oob_transfer']['url'], iq['from'])) + self._run_url_handler(iq) + iq.reply().send() diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py new file mode 100644 index 0000000..785ba36 --- /dev/null +++ b/sleekxmpp/plugins/xep_0082.py @@ -0,0 +1,204 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt +from dateutil import parser +from dateutil.tz import tzoffset, tzutc +from sleekxmpp.plugins.base import base_plugin + + +# ===================================================================== +# To make it easier for stanzas without direct access to plugin objects +# to use the XEP-0082 utility methods, we will define them as top-level +# functions and then just reference them in the plugin itself. + +def parse(time_str): + """ + Convert a string timestamp into a datetime object. + + Arguments: + time_str -- A formatted timestamp string. + """ + return parser.parse(time_str) + +def format_date(time_obj): + """ + Return a formatted string version of a date object. + + Format: + YYYY-MM-DD + + Arguments: + time_obj -- A date or datetime object. + """ + if isinstance(time_obj, dt.datetime): + time_obj = time_obj.date() + return time_obj.isoformat() + +def format_time(time_obj): + """ + Return a formatted string version of a time object. + + format: + hh:mm:ss[.sss][TZD + + arguments: + time_obj -- A time or datetime object. + """ + if isinstance(time_obj, dt.datetime): + time_obj = time_obj.timetz() + timestamp = time_obj.isoformat() + if time_obj.tzinfo == tzutc(): + timestamp = timestamp[:-6] + return '%sZ' % timestamp + return timestamp + +def format_datetime(time_obj): + """ + Return a formatted string version of a datetime object. + + Format: + YYYY-MM-DDThh:mm:ss[.sss]TZD + + arguments: + time_obj -- A datetime object. + """ + timestamp = time_obj.isoformat('T') + if time_obj.tzinfo == tzutc(): + timestamp = timestamp[:-6] + return '%sZ' % timestamp + return timestamp + +def date(year=None, month=None, day=None): + """ + Create a date only timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + year -- Integer value of the year (4 digits) + month -- Integer value of the month + day -- Integer value of the day of the month. + """ + today = dt.datetime.today() + if year is None: + year = today.year + if month is None: + month = today.month + if day is None: + day = today.day + return format_date(dt.date(year, month, day)) + +def time(hour=None, min=None, sec=None, micro=None, offset=None): + """ + Create a time only timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + hour -- Integer value of the hour. + min -- Integer value of the number of minutes. + sec -- Integer value of the number of seconds. + micro -- Integer value of the number of microseconds. + offset -- Either a positive or negative number of seconds + to offset from UTC to match a desired timezone, + or a tzinfo object. + """ + now = dt.datetime.utcnow() + if hour is None: + hour = now.hour + if min is None: + min = now.minute + if sec is None: + sec = now.second + if micro is None: + micro = now.microsecond + if offset is None: + offset = tzutc() + elif not isinstance(offset, dt.tzinfo): + offset = tzoffset(None, offset) + time = dt.time(hour, min, sec, micro, offset) + return format_time(time) + +def datetime(year=None, month=None, day=None, hour=None, + min=None, sec=None, micro=None, offset=None, + separators=True): + """ + Create a datetime timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + year -- Integer value of the year (4 digits) + month -- Integer value of the month + day -- Integer value of the day of the month. + hour -- Integer value of the hour. + min -- Integer value of the number of minutes. + sec -- Integer value of the number of seconds. + micro -- Integer value of the number of microseconds. + offset -- Either a positive or negative number of seconds + to offset from UTC to match a desired timezone, + or a tzinfo object. + """ + now = dt.datetime.utcnow() + if year is None: + year = now.year + if month is None: + month = now.month + if day is None: + day = now.day + if hour is None: + hour = now.hour + if min is None: + min = now.minute + if sec is None: + sec = now.second + if micro is None: + micro = now.microsecond + if offset is None: + offset = tzutc() + elif not isinstance(offset, dt.tzinfo): + offset = tzoffset(None, offset) + + date = dt.datetime(year, month, day, hour, + min, sec, micro, offset) + return format_datetime(date) + +class xep_0082(base_plugin): + + """ + XEP-0082: XMPP Date and Time Profiles + + XMPP uses a subset of the formats allowed by ISO 8601 as a matter of + pragmatism based on the relatively few formats historically used by + the XMPP. + + Also see . + + Methods: + date -- Create a time stamp using the Date profile. + datetime -- Create a time stamp using the DateTime profile. + time -- Create a time stamp using the Time profile. + format_date -- Format an existing date object. + format_datetime -- Format an existing datetime object. + format_time -- Format an existing time object. + parse -- Convert a time string into a Python datetime object. + """ + + def plugin_init(self): + """Start the XEP-0082 plugin.""" + self.xep = '0082' + self.description = 'XMPP Date and Time Profiles' + + self.date = date + self.datetime = datetime + self.time = time + self.format_date = format_date + self.format_datetime = format_datetime + self.format_time = format_time + self.parse = parse diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index 1ca6c15..ac0924b 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -35,7 +35,7 @@ class xep_0092(base_plugin): self.stanza = sleekxmpp.plugins.xep_0092.stanza self.name = self.config.get('name', 'SleekXMPP') - self.version = self.config.get('version', '0.1-dev') + self.version = self.config.get('version', sleekxmpp.__version__) self.os = self.config.get('os', '') self.getVersion = self.get_version diff --git a/sleekxmpp/plugins/xep_0202.py b/sleekxmpp/plugins/xep_0202.py deleted file mode 100644 index 3b31c97..0000000 --- a/sleekxmpp/plugins/xep_0202.py +++ /dev/null @@ -1,117 +0,0 @@ -""" - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. - - See the file LICENSE for copying permission. -""" - -from datetime import datetime, tzinfo -import logging -import time - -from . import base -from .. stanza.iq import Iq -from .. xmlstream.handler.callback import Callback -from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin - - -log = logging.getLogger(__name__) - - -class EntityTime(ElementBase): - name = 'time' - namespace = 'urn:xmpp:time' - plugin_attrib = 'entity_time' - interfaces = set(('tzo', 'utc')) - sub_interfaces = set(('tzo', 'utc')) - - #def get_tzo(self): - # TODO: Right now it returns a string but maybe it should - # return a datetime.tzinfo object or maybe a datetime.timedelta? - #pass - - def set_tzo(self, tzo): - if isinstance(tzo, tzinfo): - td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here' - seconds = td.seconds + td.days * 24 * 3600 - sign = ('+' if seconds >= 0 else '-') - minutes = abs(seconds // 60) - tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60) - elif not isinstance(tzo, str): - raise TypeError('The time should be a string or a datetime.tzinfo object.') - self._set_sub_text('tzo', tzo) - - def get_utc(self): - # Returns a datetime object instead the string. Is this a good idea? - value = self._get_sub_text('utc') - if '.' in value: - return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ') - else: - return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') - - def set_utc(self, tim=None): - if isinstance(tim, datetime): - if tim.utcoffset(): - tim = tim - tim.utcoffset() - tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ') - elif isinstance(tim, time.struct_time): - tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim) - elif not isinstance(tim, str): - raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.') - - self._set_sub_text('utc', tim) - - -class xep_0202(base.base_plugin): - """ - XEP-0202 Entity Time - """ - def plugin_init(self): - self.description = "Entity Time" - self.xep = "0202" - - self.xmpp.registerHandler( - Callback('Time Request', - MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns, - EntityTime.namespace)), - self.handle_entity_time_query)) - register_stanza_plugin(Iq, EntityTime) - - self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time) - - - def post_init(self): - base.base_plugin.post_init(self) - - self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time') - - def handle_entity_time_query(self, iq): - if iq['type'] == 'get': - log.debug("Entity time requested by %s" % iq['from']) - self.xmpp.event('entity_time_request', iq) - elif iq['type'] == 'result': - log.debug("Entity time result from %s" % iq['from']) - self.xmpp.event('entity_time', iq) - - def handle_entity_time(self, iq): - iq = iq.reply() - iq.enable('entity_time') - tzo = time.strftime('%z') # %z is not on all ANSI C libraries - tzo = tzo[:3] + ':' + tzo[3:] - iq['entity_time']['tzo'] = tzo - iq['entity_time']['utc'] = datetime.utcnow() - iq.send() - - def get_entity_time(self, jid): - iq = self.xmpp.makeIqGet() - iq.enable('entity_time') - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq.get('id') - result = iq.send() - if result and result is not None and result.get('type', 'error') != 'error': - return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']} - else: - return False diff --git a/sleekxmpp/plugins/xep_0202/__init__.py b/sleekxmpp/plugins/xep_0202/__init__.py new file mode 100644 index 0000000..82338d3 --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0202 import stanza +from sleekxmpp.plugins.xep_0202.stanza import EntityTime +from sleekxmpp.plugins.xep_0202.time import xep_0202 diff --git a/sleekxmpp/plugins/xep_0202/stanza.py b/sleekxmpp/plugins/xep_0202/stanza.py new file mode 100644 index 0000000..bb27692 --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/stanza.py @@ -0,0 +1,126 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt +from dateutil.tz import tzoffset, tzutc + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class EntityTime(ElementBase): + + """ + The