Merge branch 'develop'

This commit is contained in:
Lance Stout 2013-02-12 09:38:57 -08:00
commit ec5e819b16
7 changed files with 294 additions and 65 deletions

View File

@ -33,6 +33,7 @@ __all__ = [
'xep_0071', # XHTML-IM 'xep_0071', # XHTML-IM
'xep_0077', # In-Band Registration 'xep_0077', # In-Band Registration
# 'xep_0078', # Non-SASL auth. Don't automatically load # 'xep_0078', # Non-SASL auth. Don't automatically load
'xep_0079', # Advanced Message Processing
'xep_0080', # User Location 'xep_0080', # User Location
'xep_0082', # XMPP Date and Time Profiles 'xep_0082', # XMPP Date and Time Profiles
'xep_0084', # User Avatar 'xep_0084', # User Avatar

View File

@ -36,6 +36,7 @@ class XEP_0047(BasePlugin):
register_stanza_plugin(Iq, Open) register_stanza_plugin(Iq, Open)
register_stanza_plugin(Iq, Close) register_stanza_plugin(Iq, Close)
register_stanza_plugin(Iq, Data) register_stanza_plugin(Iq, Data)
register_stanza_plugin(Message, Data)
self.xmpp.register_handler(Callback( self.xmpp.register_handler(Callback(
'IBB Open', 'IBB Open',
@ -52,10 +53,16 @@ class XEP_0047(BasePlugin):
StanzaPath('iq@type=set/ibb_data'), StanzaPath('iq@type=set/ibb_data'),
self._handle_data)) self._handle_data))
self.xmpp.register_handler(Callback(
'IBB Message Data',
StanzaPath('message/ibb_data'),
self._handle_data))
def plugin_end(self): def plugin_end(self):
self.xmpp.remove_handler('IBB Open') self.xmpp.remove_handler('IBB Open')
self.xmpp.remove_handler('IBB Close') self.xmpp.remove_handler('IBB Close')
self.xmpp.remove_handler('IBB Data') self.xmpp.remove_handler('IBB Data')
self.xmpp.remove_handler('IBB Message Data')
self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb') self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb')
def session_bind(self, jid): def session_bind(self, jid):
@ -69,7 +76,7 @@ class XEP_0047(BasePlugin):
return True return True
return False return False
def open_stream(self, jid, block_size=4096, sid=None, window=1, def open_stream(self, jid, block_size=4096, sid=None, window=1, use_messages=False,
ifrom=None, block=True, timeout=None, callback=None): ifrom=None, block=True, timeout=None, callback=None):
if sid is None: if sid is None:
sid = str(uuid.uuid4()) sid = str(uuid.uuid4())
@ -83,7 +90,8 @@ class XEP_0047(BasePlugin):
iq['ibb_open']['stanza'] = 'iq' iq['ibb_open']['stanza'] = 'iq'
stream = IBBytestream(self.xmpp, sid, block_size, stream = IBBytestream(self.xmpp, sid, block_size,
iq['to'], iq['from'], window) iq['to'], iq['from'], window,
use_messages)
with self._stream_lock: with self._stream_lock:
self.pending_streams[iq['id']] = stream self.pending_streams[iq['id']] = stream
@ -139,11 +147,11 @@ class XEP_0047(BasePlugin):
self.xmpp.event('ibb_stream_start', stream) self.xmpp.event('ibb_stream_start', stream)
def _handle_data(self, iq): def _handle_data(self, stanza):
sid = iq['ibb_data']['sid'] sid = stanza['ibb_data']['sid']
stream = self.streams.get(sid, None) stream = self.streams.get(sid, None)
if stream is not None and iq['from'] != stream.sender: if stream is not None and stanza['from'] != stream.sender:
stream._recv_data(iq) stream._recv_data(stanza)
else: else:
raise XMPPError('item-not-found') raise XMPPError('item-not-found')

View File

@ -2,6 +2,7 @@ import socket
import threading import threading
import logging import logging
from sleekxmpp.stanza import Iq
from sleekxmpp.util import Queue from sleekxmpp.util import Queue
from sleekxmpp.exceptions import XMPPError from sleekxmpp.exceptions import XMPPError
@ -11,11 +12,12 @@ log = logging.getLogger(__name__)
class IBBytestream(object): class IBBytestream(object):
def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1): def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1, use_messages=False):
self.xmpp = xmpp self.xmpp = xmpp
self.sid = sid self.sid = sid
self.block_size = block_size self.block_size = block_size
self.window_size = window_size self.window_size = window_size
self.use_messages = use_messages
self.receiver = to self.receiver = to
self.sender = ifrom self.sender = ifrom
@ -46,16 +48,27 @@ class IBBytestream(object):
with self._send_seq_lock: with self._send_seq_lock:
self.send_seq = (self.send_seq + 1) % 65535 self.send_seq = (self.send_seq + 1) % 65535
seq = self.send_seq seq = self.send_seq
iq = self.xmpp.Iq() if self.use_messages:
iq['type'] = 'set' msg = self.xmpp.Message()
iq['to'] = self.receiver msg['to'] = self.receiver
iq['from'] = self.sender msg['from'] = self.sender
iq['ibb_data']['sid'] = self.sid msg['id'] = self.xmpp.new_id()
iq['ibb_data']['seq'] = seq msg['ibb_data']['sid'] = self.sid
iq['ibb_data']['data'] = data msg['ibb_data']['seq'] = seq
self.window_empty.clear() msg['ibb_data']['data'] = data
self.window_ids.add(iq['id']) msg.send()
iq.send(block=False, callback=self._recv_ack) self.send_window.release()
else:
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = self.receiver
iq['from'] = self.sender
iq['ibb_data']['sid'] = self.sid
iq['ibb_data']['seq'] = seq
iq['ibb_data']['data'] = data
self.window_empty.clear()
self.window_ids.add(iq['id'])
iq.send(block=False, callback=self._recv_ack)
return len(data) return len(data)
def sendall(self, data): def sendall(self, data):
@ -71,23 +84,25 @@ class IBBytestream(object):
if iq['type'] == 'error': if iq['type'] == 'error':
self.close() self.close()
def _recv_data(self, iq): def _recv_data(self, stanza):
with self._recv_seq_lock: with self._recv_seq_lock:
new_seq = iq['ibb_data']['seq'] new_seq = stanza['ibb_data']['seq']
if new_seq != (self.recv_seq + 1) % 65535: if new_seq != (self.recv_seq + 1) % 65535:
self.close() self.close()
raise XMPPError('unexpected-request') raise XMPPError('unexpected-request')
self.recv_seq = new_seq self.recv_seq = new_seq
data = iq['ibb_data']['data'] data = stanza['ibb_data']['data']
if len(data) > self.block_size: if len(data) > self.block_size:
self.close() self.close()
raise XMPPError('not-acceptable') raise XMPPError('not-acceptable')
self.recv_queue.put(data) self.recv_queue.put(data)
self.xmpp.event('ibb_stream_data', {'stream': self, 'data': data}) self.xmpp.event('ibb_stream_data', {'stream': self, 'data': data})
iq.reply()
iq.send() if isinstance(stanza, Iq):
stanza.reply()
stanza.send()
def recv(self, *args, **kwargs): def recv(self, *args, **kwargs):
return self.read(block=True) return self.read(block=True)

View File

@ -0,0 +1,18 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0079.stanza import (
AMP, Rule, InvalidRules, UnsupportedConditions,
UnsupportedActions, FailedRules, FailedRule,
AMPFeature)
from sleekxmpp.plugins.xep_0079.amp import XEP_0079
register_plugin(XEP_0079)

View File

@ -0,0 +1,79 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permissio
"""
import logging
from sleekxmpp.stanza import Message, Error, StreamFeatures
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.matcher import StanzaPath, MatchMany
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0079 import stanza
log = logging.getLogger(__name__)
class XEP_0079(BasePlugin):
"""
XEP-0079 Advanced Message Processing
"""
name = 'xep_0079'
description = 'XEP-0079: Advanced Message Processing'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, stanza.AMP)
register_stanza_plugin(Error, stanza.InvalidRules)
register_stanza_plugin(Error, stanza.UnsupportedConditions)
register_stanza_plugin(Error, stanza.UnsupportedActions)
register_stanza_plugin(Error, stanza.FailedRules)
self.xmpp.register_handler(
Callback('AMP Response',
MatchMany([
StanzaPath('message/error/failed_rules'),
StanzaPath('message/amp')
]),
self._handle_amp_response))
if not self.xmpp.is_component:
self.xmpp.register_feature('amp',
self._handle_amp_feature,
restart=False,
order=9000)
register_stanza_plugin(StreamFeatures, stanza.AMPFeature)
def plugin_end(self):
self.xmpp.remove_handler('AMP Response')
def _handle_amp_response(self, msg):
log.debug('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
if msg['type'] == 'error':
self.xmpp.event('amp_error', msg)
elif msg['amp']['status'] in ('alert', 'notify'):
self.xmpp.event('amp_%s' % msg['amp']['status'], msg)
def _handle_amp_feature(self, features):
log.debug('Advanced Message Processing is available.')
self.xmpp.features.add('amp')
def discover_support(self, jid=None, **iqargs):
if jid is None:
if self.xmpp.is_component:
jid = self.xmpp.server_host
else:
jid = self.xmpp.boundjid.host
return self.xmpp['xep_0030'].get_info(
jid=jid,
node='http://jabber.org/protocol/amp',
**iqargs)

View File

@ -0,0 +1,96 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from __future__ import unicode_literals
from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
class AMP(ElementBase):
namespace = 'http://jabber.org/protocol/amp'
name = 'amp'
plugin_attrib = 'amp'
interfaces = set(['from', 'to', 'status', 'per_hop'])
def get_from(self):
return JID(self._get_attr('from'))
def set_from(self, value):
return self._set_attr('from', str(value))
def get_to(self):
return JID(self._get_attr('from'))
def set_to(self, value):
return self._set_attr('to', str(value))
def get_per_hop(self):
return self._get_attr('per-hop') == 'true'
def set_per_hop(self, value):
if value:
return self._set_attr('per-hop', 'true')
else:
return self._del_attr('per-hop')
def del_per_hop(self):
return self._del_attr('per-hop')
def add_rule(self, action, condition, value):
rule = Rule(parent=self)
rule['action'] = action
rule['condition'] = condition
rule['value'] = value
class Rule(ElementBase):
namespace = 'http://jabber.org/protocol/amp'
name = 'rule'
plugin_attrib = name
plugin_multi_attrib = 'rules'
interfaces = set(['action', 'condition', 'value'])
class InvalidRules(ElementBase):
namespace = 'http://jabber.org/protocol/amp'
name = 'invalid-rules'
plugin_attrib = 'invalid_rules'
class UnsupportedConditions(ElementBase):
namespace = 'http://jabber.org/protocol/amp'
name = 'unsupported-conditions'
plugin_attrib = 'unsupported_conditions'
class UnsupportedActions(ElementBase):
namespace = 'http://jabber.org/protocol/amp'
name = 'unsupported-actions'
plugin_attrib = 'unsupported_actions'
class FailedRule(Rule):
namespace = 'http://jabber.org/protocol/amp#errors'
class FailedRules(ElementBase):
namespace = 'http://jabber.org/protocol/amp#errors'
name = 'failed-rules'
plugin_attrib = 'failed_rules'
class AMPFeature(ElementBase):
namespace = 'http://jabber.org/features/amp'
name = 'amp'
register_stanza_plugin(AMP, Rule, iterable=True)
register_stanza_plugin(InvalidRules, Rule, iterable=True)
register_stanza_plugin(UnsupportedConditions, Rule, iterable=True)
register_stanza_plugin(UnsupportedActions, Rule, iterable=True)
register_stanza_plugin(FailedRules, FailedRule, iterable=True)

View File

@ -9,8 +9,9 @@
import logging import logging
import hashlib import hashlib
import base64 import base64
import threading
import sleekxmpp from sleekxmpp import __version__
from sleekxmpp.stanza import StreamFeatures, Presence, Iq from sleekxmpp.stanza import StreamFeatures, Presence, Iq
from sleekxmpp.xmlstream import register_stanza_plugin, JID from sleekxmpp.xmlstream import register_stanza_plugin, JID
from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.handler import Callback
@ -45,8 +46,7 @@ class XEP_0115(BasePlugin):
'md5': hashlib.md5} 'md5': hashlib.md5}
if self.caps_node is None: if self.caps_node is None:
ver = sleekxmpp.__version__ self.caps_node = 'http://sleekxmpp.com/ver/%s' % __version__
self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver
register_stanza_plugin(Presence, stanza.Capabilities) register_stanza_plugin(Presence, stanza.Capabilities)
register_stanza_plugin(StreamFeatures, stanza.Capabilities) register_stanza_plugin(StreamFeatures, stanza.Capabilities)
@ -90,6 +90,9 @@ class XEP_0115(BasePlugin):
disco.assign_verstring = self.assign_verstring disco.assign_verstring = self.assign_verstring
disco.get_verstring = self.get_verstring disco.get_verstring = self.get_verstring
self._processing_lock = threading.Lock()
self._processing = set()
def plugin_end(self): def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace) self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace)
self.xmpp.del_filter('out', self._filter_add_caps) self.xmpp.del_filter('out', self._filter_add_caps)
@ -135,17 +138,22 @@ class XEP_0115(BasePlugin):
def _process_caps(self, pres): def _process_caps(self, pres):
if not pres['caps']['hash']: if not pres['caps']['hash']:
log.debug("Received unsupported legacy caps.") log.debug("Received unsupported legacy caps: %s, %s, %s",
pres['caps']['node'],
pres['caps']['ver'],
pres['caps']['ext'])
self.xmpp.event('entity_caps_legacy', pres) self.xmpp.event('entity_caps_legacy', pres)
return return
ver = pres['caps']['ver']
existing_verstring = self.get_verstring(pres['from'].full) existing_verstring = self.get_verstring(pres['from'].full)
if str(existing_verstring) == str(pres['caps']['ver']): if str(existing_verstring) == str(ver):
return return
existing_caps = self.get_caps(verstring=pres['caps']['ver']) existing_caps = self.get_caps(verstring=ver)
if existing_caps is not None: if existing_caps is not None:
self.assign_verstring(pres['from'], pres['caps']['ver']) self.assign_verstring(pres['from'], ver)
return return
if pres['caps']['hash'] not in self.hashes: if pres['caps']['hash'] not in self.hashes:
@ -156,9 +164,16 @@ class XEP_0115(BasePlugin):
except XMPPError: except XMPPError:
return return
log.debug("New caps verification string: %s", pres['caps']['ver']) # Only lookup the same caps once at a time.
with self._processing_lock:
if ver in self._processing:
log.debug('Already processing verstring %s' % ver)
return
self._processing.add(ver)
log.debug("New caps verification string: %s", ver)
try: try:
node = '%s#%s' % (pres['caps']['node'], pres['caps']['ver']) node = '%s#%s' % (pres['caps']['node'], ver)
caps = self.xmpp['xep_0030'].get_info(pres['from'], node) caps = self.xmpp['xep_0030'].get_info(pres['from'], node)
if isinstance(caps, Iq): if isinstance(caps, Iq):
@ -168,7 +183,10 @@ class XEP_0115(BasePlugin):
pres['caps']['ver']): pres['caps']['ver']):
self.assign_verstring(pres['from'], pres['caps']['ver']) self.assign_verstring(pres['from'], pres['caps']['ver'])
except XMPPError: except XMPPError:
log.debug("Could not retrieve disco#info results for caps") log.debug("Could not retrieve disco#info results for caps for %s", node)
with self._processing_lock:
self._processing.remove(ver)
def _validate_caps(self, caps, hash, check_verstring): def _validate_caps(self, caps, hash, check_verstring):
# Check Identities # Check Identities
@ -179,7 +197,6 @@ class XEP_0115(BasePlugin):
return False return False
# Check Features # Check Features
full_features = caps.get_features(dedupe=False) full_features = caps.get_features(dedupe=False)
deduped_features = caps.get_features() deduped_features = caps.get_features()
if len(full_features) != len(deduped_features): if len(full_features) != len(deduped_features):
@ -190,29 +207,32 @@ class XEP_0115(BasePlugin):
form_types = [] form_types = []
deduped_form_types = set() deduped_form_types = set()
for stanza in caps['substanzas']: for stanza in caps['substanzas']:
if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): if not isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
if 'FORM_TYPE' in stanza['fields']: log.debug("Non form extension found, ignoring for caps")
f_type = tuple(stanza['fields']['FORM_TYPE']['value']) caps.xml.remove(stanza.xml)
form_types.append(f_type) continue
deduped_form_types.add(f_type) if 'FORM_TYPE' in stanza['fields']:
if len(form_types) != len(deduped_form_types): f_type = tuple(stanza['fields']['FORM_TYPE']['value'])
log.debug("Duplicated FORM_TYPE values, " + \ form_types.append(f_type)
"invalid for caps") deduped_form_types.add(f_type)
if len(form_types) != len(deduped_form_types):
log.debug("Duplicated FORM_TYPE values, " + \
"invalid for caps")
return False
if len(f_type) > 1:
deduped_type = set(f_type)
if len(f_type) != len(deduped_type):
log.debug("Extra FORM_TYPE data, invalid for caps")
return False return False
if len(f_type) > 1: if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
deduped_type = set(f_type) log.debug("Field FORM_TYPE type not 'hidden', " + \
if len(f_type) != len(deduped_type): "ignoring form for caps")
log.debug("Extra FORM_TYPE data, invalid for caps")
return False
if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
log.debug("Field FORM_TYPE type not 'hidden', " + \
"ignoring form for caps")
caps.xml.remove(stanza.xml)
else:
log.debug("No FORM_TYPE found, ignoring form for caps")
caps.xml.remove(stanza.xml) caps.xml.remove(stanza.xml)
else:
log.debug("No FORM_TYPE found, ignoring form for caps")
caps.xml.remove(stanza.xml)
verstring = self.generate_verstring(caps, hash) verstring = self.generate_verstring(caps, hash)
if verstring != check_verstring: if verstring != check_verstring:
@ -272,7 +292,7 @@ class XEP_0115(BasePlugin):
binary = hash(S.encode('utf8')).digest() binary = hash(S.encode('utf8')).digest()
return base64.b64encode(binary).decode('utf-8') return base64.b64encode(binary).decode('utf-8')
def update_caps(self, jid=None, node=None): def update_caps(self, jid=None, node=None, preserve=False):
try: try:
info = self.xmpp['xep_0030'].get_info(jid, node, local=True) info = self.xmpp['xep_0030'].get_info(jid, node, local=True)
if isinstance(info, Iq): if isinstance(info, Iq):
@ -286,19 +306,11 @@ class XEP_0115(BasePlugin):
self.assign_verstring(jid, ver) self.assign_verstring(jid, ver)
if self.xmpp.session_started_event.is_set() and self.broadcast: if self.xmpp.session_started_event.is_set() and self.broadcast:
# Check if we've sent directed presence. If we haven't, we if self.xmpp.is_component or preserve:
# can just send a normal presence stanza. If we have, then
# we will send presence to each contact individually so
# that we don't clobber existing statuses.
directed = False or self.xmpp.is_component
for contact in self.xmpp.roster[jid]:
if self.xmpp.roster[jid][contact].last_status is not None:
directed = True
if not directed:
self.xmpp.roster[jid].send_last_presence()
else:
for contact in self.xmpp.roster[jid]: for contact in self.xmpp.roster[jid]:
self.xmpp.roster[jid][contact].send_last_presence() self.xmpp.roster[jid][contact].send_last_presence()
else:
self.xmpp.roster[jid].send_last_presence()
except XMPPError: except XMPPError:
return return