Merge branch 'develop' into roster

This commit is contained in:
Lance Stout 2011-07-27 19:35:42 -07:00
commit ad978700fc
20 changed files with 1037 additions and 164 deletions

167
examples/proxy_echo_client.py Executable file
View file

@ -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.")

View file

@ -15,5 +15,5 @@ from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
__version__ = '1.0beta5' __version__ = '1.0beta6'
__version_info__ = (1, 0, 0, 'beta5', 0) __version_info__ = (1, 0, 0, 'beta6', 0)

View file

@ -168,18 +168,23 @@ class ClientXMPP(BaseXMPP):
addresses = {} addresses = {}
intmax = 0 intmax = 0
topprio = 65535
for answer in answers: for answer in answers:
intmax += answer.priority topprio = min(topprio, answer.priority)
addresses[intmax] = (answer.target.to_text()[:-1], for answer in answers:
if answer.priority == topprio:
intmax += answer.weight
addresses[intmax] = (answer.target.to_text()[:-1],
answer.port) answer.port)
#python3 returns a generator for dictionary keys #python3 returns a generator for dictionary keys
priorities = [x for x in addresses.keys()] items = [x for x in addresses.keys()]
priorities.sort() items.sort()
picked = random.randint(0, intmax) picked = random.randint(0, intmax)
for priority in priorities: for item in items:
if picked <= priority: if picked <= item:
address = addresses[priority] address = addresses[item]
break break
if not address: if not address:

View file

@ -6,5 +6,6 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086', 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082',
'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify'] 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199',
'xep_0202', 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify']

View file

@ -6,8 +6,10 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
import logging
from sleekxmpp.stanza import Message, Presence, Iq from sleekxmpp.stanza import Message, Presence, Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream.matcher import StanzaPath
@ -15,6 +17,9 @@ from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins.xep_0066 import stanza from sleekxmpp.plugins.xep_0066 import stanza
log = logging.getLogger(__name__)
class xep_0066(base_plugin): class xep_0066(base_plugin):
""" """
@ -43,6 +48,9 @@ class xep_0066(base_plugin):
self.description = 'Out-of-Band Transfer' self.description = 'Out-of-Band Transfer'
self.stanza = stanza self.stanza = stanza
self.url_handlers = {'global': self._default_handler,
'jid': {}}
register_stanza_plugin(Iq, stanza.OOBTransfer) register_stanza_plugin(Iq, stanza.OOBTransfer)
register_stanza_plugin(Message, stanza.OOB) register_stanza_plugin(Message, stanza.OOB)
register_stanza_plugin(Presence, 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.OOBTransfer.namespace)
self.xmpp['xep_0030'].add_feature(stanza.OOB.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): def send_oob(self, to, url, desc=None, ifrom=None, **iqargs):
""" """
Initiate a basic file transfer by sending the URL of Initiate a basic file transfer by sending the URL of
@ -84,6 +114,41 @@ class xep_0066(base_plugin):
iq['oob_transfer']['desc'] = desc iq['oob_transfer']['desc'] = desc
return iq.send(**iqargs) 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): 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()

View file

@ -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 <http://www.xmpp.org/extensions/xep-0082.html>.
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

View file

@ -35,7 +35,7 @@ class xep_0092(base_plugin):
self.stanza = sleekxmpp.plugins.xep_0092.stanza self.stanza = sleekxmpp.plugins.xep_0092.stanza
self.name = self.config.get('name', 'SleekXMPP') 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.os = self.config.get('os', '')
self.getVersion = self.get_version self.getVersion = self.get_version

View file

@ -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

View file

@ -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

View file

@ -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 <time> element represents the local time for an XMPP agent.
The time is expressed in UTC to make synchronization easier
between entities, but the offset for the local timezone is also
included.
Example <time> stanzas:
<iq type="result">
<time xmlns="urn:xmpp:time">
<utc>2011-07-03T11:37:12.234569</utc>
<tzo>-07:00</tzo>
</time>
</iq>
Stanza Interface:
time -- The local time for the entity (updates utc and tzo).
utc -- The UTC equivalent to local time.
tzo -- The local timezone offset from UTC.
Methods:
get_time -- Return local time datetime object.
set_time -- Set UTC and TZO fields.
del_time -- Remove both UTC and TZO fields.
get_utc -- Return datetime object of UTC time.
set_utc -- Set the UTC time.
get_tzo -- Return tzinfo object.
set_tzo -- Set the local timezone offset.
"""
name = 'time'
namespace = 'urn:xmpp:time'
plugin_attrib = 'entity_time'
interfaces = set(('tzo', 'utc', 'time'))
sub_interfaces = interfaces
def set_time(self, value):
"""
Set both the UTC and TZO fields given a time object.
Arguments:
value -- A datetime object or properly formatted
string equivalent.
"""
date = value
if not isinstance(value, dt.datetime):
date = xep_0082.parse(value)
self['utc'] = date
self['tzo'] = date.tzinfo
def get_time(self):
"""
Return the entity's local time based on the UTC and TZO data.
"""
date = self['utc']
tz = self['tzo']
return date.astimezone(tz)
def del_time(self):
"""Remove both the UTC and TZO fields."""
del self['utc']
del self['tzo']
def get_tzo(self):
"""
Return the timezone offset from UTC as a tzinfo object.
"""
tzo = self._get_sub_text('tzo')
if tzo == '':
tzo = 'Z'
time = xep_0082.parse('00:00:00%s' % tzo)
return time.tzinfo
def set_tzo(self, value):
"""
Set the timezone offset from UTC.
Arguments:
value -- Either a tzinfo object or the number of
seconds (positive or negative) to offset.
"""
time = xep_0082.time(offset=value)
if xep_0082.parse(time).tzinfo == tzutc():
self._set_sub_text('tzo', 'Z')
else:
self._set_sub_text('tzo', time[-6:])
def get_utc(self):
"""
Return the time in UTC as a datetime object.
"""
value = self._get_sub_text('utc')
if value == '':
return xep_0082.parse(xep_0082.datetime())
return xep_0082.parse('%sZ' % value)
def set_utc(self, value):
"""
Set the time in UTC.
Arguments:
value -- A datetime object or properly formatted
string equivalent.
"""
date = value
if not isinstance(value, dt.datetime):
date = xep_0082.parse(value)
date = date.astimezone(tzutc())
value = xep_0082.format_datetime(date)[:-1]
self._set_sub_text('utc', value)

View file

@ -0,0 +1,92 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from sleekxmpp.stanza.iq import Iq
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins import xep_0082
from sleekxmpp.plugins.xep_0202 import stanza
log = logging.getLogger(__name__)
class xep_0202(base_plugin):
"""
XEP-0202: Entity Time
"""
def plugin_init(self):
"""Start the XEP-0203 plugin."""
self.xep = '0202'
self.description = 'Entity Time'
self.stanza = stanza
self.tz_offset = self.config.get('tz_offset', 0)
# As a default, respond to time requests with the
# local time returned by XEP-0082. However, a
# custom function can be supplied which accepts
# the JID of the entity to query for the time.
self.local_time = self.config.get('local_time', None)
if not self.local_time:
self.local_time = lambda x: xep_0082.datetime(offset=self.tz_offset)
self.xmpp.registerHandler(
Callback('Entity Time',
StanzaPath('iq/entity_time'),
self._handle_time_request))
register_stanza_plugin(Iq, stanza.EntityTime)
def post_init(self):
"""Handle cross-plugin interactions."""
base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
def _handle_time_request(self, iq):
"""
Respond to a request for the local time.
The time is taken from self.local_time(), which may be replaced
during plugin configuration with a function that maps JIDs to
times.
Arguments:
iq -- The Iq time request stanza.
"""
iq.reply()
iq['entity_time']['time'] = self.local_time(iq['to'])
iq.send()
def get_entity_time(self, to, ifrom=None, **iqargs):
"""
Request the time from another entity.
Arguments:
to -- JID of the entity to query.
ifrom -- Specifiy the sender's JID.
block -- If true, block and wait for the stanzas' reply.
timeout -- The time in seconds to block while waiting for
a reply. If None, then wait indefinitely.
callback -- Optional callback to execute when a reply is
received instead of blocking and waiting for
the reply.
"""
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = 'to'
if ifrom:
iq['from'] = 'ifrom'
iq.enable('entity_time')
return iq.send(**iqargs)

View file

@ -0,0 +1,12 @@
"""
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_0203 import stanza
from sleekxmpp.plugins.xep_0203.stanza import Delay
from sleekxmpp.plugins.xep_0203.delay import xep_0203

View file

@ -0,0 +1,36 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.stanza import Message, Presence
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins.xep_0203 import stanza
class xep_0203(base_plugin):
"""
XEP-0203: Delayed Delivery
XMPP stanzas are sometimes withheld for delivery due to the recipient
being offline, or are resent in order to establish recent history as
is the case with MUCS. In any case, it is important to know when the
stanza was originally sent, not just when it was last received.
Also see <http://www.xmpp.org/extensions/xep-0203.html>.
"""
def plugin_init(self):
"""Start the XEP-0203 plugin."""
self.xep = '0203'
self.description = 'Delayed Delivery'
self.stanza = stanza
register_stanza_plugin(Message, stanza.Delay)
register_stanza_plugin(Presence, stanza.Delay)

View file

@ -0,0 +1,41 @@
"""
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 sleekxmpp.xmlstream import ElementBase
from sleekxmpp.plugins import xep_0082
class Delay(ElementBase):
"""
"""
name = 'delay'
namespace = 'urn:xmpp:delay'
plugin_attrib = 'delay'
interfaces = set(('from', 'stamp', 'text'))
def get_stamp(self):
timestamp = self._get_attr('stamp')
return xep_0082.parse(timestamp)
def set_stamp(self, value):
if isinstance(value, dt.datetime):
value = xep_0082.format_datetime(value)
self._set_attr('stamp', value)
def get_text(self):
return self.xml.text
def set_text(self, value):
self.xml.text = value
def del_text(self):
self.xml.text = ''

View file

@ -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_0224 import stanza
from sleekxmpp.plugins.xep_0224.stanza import Attention
from sleekxmpp.plugins.xep_0224.attention import xep_0224

View file

@ -0,0 +1,72 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from sleekxmpp.stanza import Message
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins.xep_0224 import stanza
log = logging.getLogger(__name__)
class xep_0224(base_plugin):
"""
XEP-0224: Attention
"""
def plugin_init(self):
"""Start the XEP-0224 plugin."""
self.xep = '0224'
self.description = 'Attention'
self.stanza = stanza
register_stanza_plugin(Message, stanza.Attention)
self.xmpp.register_handler(
Callback('Attention',
StanzaPath('message/attention'),
self._handle_attention))
def post_init(self):
"""Handle cross-plugin dependencies."""
base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace)
def request_attention(self, to, mfrom=None, mbody=''):
"""
Send an attention message with an optional body.
Arguments:
to -- The attention request recipient's JID.
mfrom -- Optionally specify the sender of the attention request.
mbody -- An optional message body to include in the request.
"""
m = self.xmpp.Message()
m['to'] = to
m['type'] = 'headline'
m['attention'] = True
if mfrom:
m['from'] = mfrom
m['body'] = mbody
m.send()
def _handle_attention(self, msg):
"""
Raise an event after receiving a message with an attention request.
Arguments:
msg -- A message stanza with an attention element.
"""
log.debug("Received attention request from: %s" % msg['from'])
self.xmpp.event('attention', msg)

View file

@ -0,0 +1,40 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase, ET
class Attention(ElementBase):
"""
"""
name = 'attention'
namespace = 'urn:xmpp:attention:0'
plugin_attrib = 'attention'
interfaces = set(('attention',))
is_extension = True
def setup(self, xml):
return True
def set_attention(self, value):
if value:
xml = ET.Element(self.tag_name())
self.parent().xml.append(xml)
else:
self.del_attention()
def get_attention(self):
xml = self.parent().xml.find(self.tag_name())
return xml is not None
def del_attention(self):
xml = self.parent().xml.find(self.tag_name())
if xml is not None:
self.parent().xml.remove(xml)

View file

@ -8,6 +8,7 @@
from __future__ import with_statement, unicode_literals from __future__ import with_statement, unicode_literals
import base64
import copy import copy
import logging import logging
import signal import signal
@ -23,6 +24,7 @@ try:
except ImportError: except ImportError:
import Queue as queue import Queue as queue
import sleekxmpp
from sleekxmpp.thirdparty.statemachine import StateMachine from sleekxmpp.thirdparty.statemachine import StateMachine
from sleekxmpp.xmlstream import Scheduler, tostring from sleekxmpp.xmlstream import Scheduler, tostring
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
@ -107,7 +109,13 @@ class XMLStream(object):
stream_header -- The closing tag of the stream's root element. stream_header -- The closing tag of the stream's root element.
use_ssl -- Flag indicating if SSL should be used. use_ssl -- Flag indicating if SSL should be used.
use_tls -- Flag indicating if TLS should be used. use_tls -- Flag indicating if TLS should be used.
use_proxy -- Flag indicating that an HTTP Proxy should be used.
stop -- threading Event used to stop all threads. stop -- threading Event used to stop all threads.
proxy_config -- An optional dictionary with the following entries:
host -- The host offering proxy services.
port -- The port for the proxy service.
username -- Optional username for the proxy.
password -- Optional password for the proxy.
auto_reconnect -- Flag to determine whether we auto reconnect. auto_reconnect -- Flag to determine whether we auto reconnect.
reconnect_max_delay -- Maximum time to delay between connection reconnect_max_delay -- Maximum time to delay between connection
@ -180,6 +188,9 @@ class XMLStream(object):
self.use_ssl = False self.use_ssl = False
self.use_tls = False self.use_tls = False
self.use_proxy = False
self.proxy_config = {}
self.default_ns = '' self.default_ns = ''
self.stream_header = "<stream>" self.stream_header = "<stream>"
@ -322,6 +333,12 @@ class XMLStream(object):
log.debug('Waiting %s seconds before connecting.' % delay) log.debug('Waiting %s seconds before connecting.' % delay)
time.sleep(delay) time.sleep(delay)
if self.use_proxy:
connected = self._connect_proxy()
if not connected:
self.reconnect_delay = delay
return False
if self.use_ssl and self.ssl_support: if self.use_ssl and self.ssl_support:
log.debug("Socket Wrapped for SSL") log.debug("Socket Wrapped for SSL")
if self.ca_certs is None: if self.ca_certs is None:
@ -341,8 +358,10 @@ class XMLStream(object):
self.socket = ssl_socket self.socket = ssl_socket
try: try:
log.debug("Connecting to %s:%s" % self.address) if not self.use_proxy:
self.socket.connect(self.address) log.debug("Connecting to %s:%s" % self.address)
self.socket.connect(self.address)
self.set_socket(self.socket, ignore=True) self.set_socket(self.socket, ignore=True)
#this event is where you should set your application state #this event is where you should set your application state
self.event("connected", direct=True) self.event("connected", direct=True)
@ -356,22 +375,86 @@ class XMLStream(object):
self.reconnect_delay = delay self.reconnect_delay = delay
return False return False
def disconnect(self, reconnect=False): def _connect_proxy(self):
"""Attempt to connect using an HTTP Proxy."""
# Extract the proxy address, and optional credentials
address = (self.proxy_config['host'], int(self.proxy_config['port']))
cred = None
if self.proxy_config['username']:
username = self.proxy_config['username']
password = self.proxy_config['password']
cred = '%s:%s' % (username, password)
if sys.version_info < (3, 0):
cred = bytes(cred)
else:
cred = bytes(cred, 'utf-8')
cred = base64.b64encode(cred).decode('utf-8')
# Build the HTTP headers for connecting to the XMPP server
headers = ['CONNECT %s:%s HTTP/1.0' % self.address,
'Host: %s:%s' % self.address,
'Proxy-Connection: Keep-Alive',
'Pragma: no-cache',
'User-Agent: SleekXMPP/%s' % sleekxmpp.__version__]
if cred:
headers.append('Proxy-Authorization: Basic %s' % cred)
headers = '\r\n'.join(headers) + '\r\n\r\n'
try:
log.debug("Connecting to proxy: %s:%s" % address)
self.socket.connect(address)
self.send_raw(headers, now=True)
resp = ''
while '\r\n\r\n' not in resp:
resp += self.socket.recv(1024).decode('utf-8')
log.debug('RECV: %s' % resp)
lines = resp.split('\r\n')
if '200' not in lines[0]:
self.event('proxy_error', resp)
log.error('Proxy Error: %s' % lines[0])
return False
# Proxy connection established, continue connecting
# with the XMPP server.
return True
except Socket.error as serr:
error_msg = "Could not connect to %s:%s. Socket Error #%s: %s"
self.event('socket_error', serr)
log.error(error_msg % (self.address[0], self.address[1],
serr.errno, serr.strerror))
return False
def disconnect(self, reconnect=False, wait=False):
""" """
Terminate processing and close the XML streams. Terminate processing and close the XML streams.
Optionally, the connection may be reconnected and Optionally, the connection may be reconnected and
resume processing afterwards. resume processing afterwards.
If the disconnect should take place after all items
in the send queue have been sent, use wait=True. However,
take note: If you are constantly adding items to the queue
such that it is never empty, then the disconnect will
not occur and the call will continue to block.
Arguments: Arguments:
reconnect -- Flag indicating if the connection reconnect -- Flag indicating if the connection
and processing should be restarted. and processing should be restarted.
Defaults to False. Defaults to False.
wait -- Flag indicating if the send queue should
be emptied before disconnecting.
""" """
self.state.transition('connected', 'disconnected', wait=0.0, self.state.transition('connected', 'disconnected', wait=0.0,
func=self._disconnect, args=(reconnect,)) func=self._disconnect, args=(reconnect, wait))
def _disconnect(self, reconnect=False, wait=False):
# Wait for the send queue to empty.
if wait:
self.send_queue.join()
def _disconnect(self, reconnect=False):
# Send the end of stream marker. # Send the end of stream marker.
self.send_raw(self.stream_footer, now=True) self.send_raw(self.stream_footer, now=True)
self.session_started_event.clear() self.session_started_event.clear()
@ -1036,6 +1119,7 @@ class XMLStream(object):
log.debug("SEND: %s" % data) log.debug("SEND: %s" % data)
try: try:
self.socket.send(data.encode('utf-8')) self.socket.send(data.encode('utf-8'))
self.send_queue.task_done()
except Socket.error as serr: except Socket.error as serr:
self.event('socket_error', serr) self.event('socket_error', serr)
log.warning("Failed to send %s" % data) log.warning("Failed to send %s" % data)

View file

@ -200,5 +200,56 @@ class TestStreamPresence(SleekTest):
self.assertEqual(events, expected, self.assertEqual(events, expected,
"Incorrect events triggered: %s" % events) "Incorrect events triggered: %s" % events)
def test_presence_events(self):
"""Test that presence events are raised."""
events = []
self.stream_start()
ptypes = ['available', 'away', 'dnd', 'xa', 'chat',
'unavailable', 'subscribe', 'subscribed',
'unsubscribe', 'unsubscribed']
for ptype in ptypes:
handler = lambda p: events.append(p['type'])
self.xmpp.add_event_handler('presence_%s' % ptype, handler)
self.recv("""
<presence />
""")
self.recv("""
<presence><show>away</show></presence>
""")
self.recv("""
<presence><show>dnd</show></presence>
""")
self.recv("""
<presence><show>xa</show></presence>
""")
self.recv("""
<presence><show>chat</show></presence>
""")
self.recv("""
<presence type="unavailable" />
""")
self.recv("""
<presence type="subscribe" />
""")
self.recv("""
<presence type="subscribed" />
""")
self.recv("""
<presence type="unsubscribe" />
""")
self.recv("""
<presence type="unsubscribed" />
""")
time.sleep(.5)
self.assertEqual(events, ptypes,
"Not all events raised: %s" % events)
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence)

View file

@ -40,33 +40,5 @@ class TestOOB(SleekTest):
t.join() t.join()
def testReceiveOOB(self):
"""Test receiving an OOB request."""
self.stream_start(plugins=['xep_0066', 'xep_0030'])
events = []
def receive_oob(iq):
events.append(iq['oob_transfer']['url'])
self.xmpp.add_event_handler('oob_transfer', receive_oob)
self.recv("""
<iq to="tester@localhost"
from="user@example.com"
type="set" id="1">
<query xmlns="jabber:iq:oob">
<url>http://github.com/fritzy/SleekXMPP/blob/master/README</url>
<desc>SleekXMPP README</desc>
</query>
</iq>
""")
time.sleep(0.1)
self.assertEqual(events,
['http://github.com/fritzy/SleekXMPP/blob/master/README'],
'URL was not received: %s' % events)
suite = unittest.TestLoader().loadTestsFromTestCase(TestOOB) suite = unittest.TestLoader().loadTestsFromTestCase(TestOOB)