mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-24 03:00:15 +00:00
Merge branch 'develop' into roster
This commit is contained in:
commit
ad978700fc
20 changed files with 1037 additions and 164 deletions
167
examples/proxy_echo_client.py
Executable file
167
examples/proxy_echo_client.py
Executable 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.")
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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()
|
||||||
|
|
204
sleekxmpp/plugins/xep_0082.py
Normal file
204
sleekxmpp/plugins/xep_0082.py
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
11
sleekxmpp/plugins/xep_0202/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0202/__init__.py
Normal 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
|
126
sleekxmpp/plugins/xep_0202/stanza.py
Normal file
126
sleekxmpp/plugins/xep_0202/stanza.py
Normal 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)
|
92
sleekxmpp/plugins/xep_0202/time.py
Normal file
92
sleekxmpp/plugins/xep_0202/time.py
Normal 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)
|
12
sleekxmpp/plugins/xep_0203/__init__.py
Normal file
12
sleekxmpp/plugins/xep_0203/__init__.py
Normal 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
|
||||||
|
|
36
sleekxmpp/plugins/xep_0203/delay.py
Normal file
36
sleekxmpp/plugins/xep_0203/delay.py
Normal 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)
|
41
sleekxmpp/plugins/xep_0203/stanza.py
Normal file
41
sleekxmpp/plugins/xep_0203/stanza.py
Normal 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 = ''
|
11
sleekxmpp/plugins/xep_0224/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0224/__init__.py
Normal 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
|
72
sleekxmpp/plugins/xep_0224/attention.py
Normal file
72
sleekxmpp/plugins/xep_0224/attention.py
Normal 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)
|
40
sleekxmpp/plugins/xep_0224/stanza.py
Normal file
40
sleekxmpp/plugins/xep_0224/stanza.py
Normal 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)
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue