mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-30 19:19:55 +00:00
Merge branch 'develop' into roster
This commit is contained in:
commit
adade2e5ec
17 changed files with 966 additions and 424 deletions
198
examples/disco_browser.py
Executable file
198
examples/disco_browser.py
Executable file
|
@ -0,0 +1,198 @@
|
||||||
|
#!/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 time
|
||||||
|
import logging
|
||||||
|
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 Disco(sleekxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A demonstration for using basic service discovery.
|
||||||
|
|
||||||
|
Send a disco#info and disco#items request to a JID/node combination,
|
||||||
|
and print out the results.
|
||||||
|
|
||||||
|
May also request only particular info categories such as just features,
|
||||||
|
or just items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, jid, password, target_jid, target_node='', get=''):
|
||||||
|
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||||
|
|
||||||
|
# Using service discovery requires the XEP-0030 plugin.
|
||||||
|
self.register_plugin('xep_0030')
|
||||||
|
|
||||||
|
self.get = get
|
||||||
|
self.target_jid = target_jid
|
||||||
|
self.target_node = target_node
|
||||||
|
|
||||||
|
# Values to control which disco entities are reported
|
||||||
|
self.info_types = ['', 'all', 'info', 'identities', 'features']
|
||||||
|
self.identity_types = ['', 'all', 'info', 'identities']
|
||||||
|
self.feature_types = ['', 'all', 'info', 'features']
|
||||||
|
self.items_types = ['', 'all', 'items']
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
In this case, we send disco#info and disco#items
|
||||||
|
stanzas to the requested JID and print the results.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
event -- An empty dictionary. The session_start
|
||||||
|
event does not provide any additional
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
self.get_roster()
|
||||||
|
self.send_presence()
|
||||||
|
|
||||||
|
if self.get in self.info_types:
|
||||||
|
# By using block=True, the result stanza will be
|
||||||
|
# returned. Execution will block until the reply is
|
||||||
|
# received. Non-blocking options would be to listen
|
||||||
|
# for the disco_info event, or passing a handler
|
||||||
|
# function using the callback parameter.
|
||||||
|
info = self['xep_0030'].get_info(jid=self.target_jid,
|
||||||
|
node=self.target_node,
|
||||||
|
block=True)
|
||||||
|
if self.get in self.items_types:
|
||||||
|
# The same applies from above. Listen for the
|
||||||
|
# disco_items event or pass a callback function
|
||||||
|
# if you need to process a non-blocking request.
|
||||||
|
items = self['xep_0030'].get_items(jid=self.target_jid,
|
||||||
|
node=self.target_node,
|
||||||
|
block=True)
|
||||||
|
else:
|
||||||
|
logging.error("Invalid disco request type.")
|
||||||
|
self.disconnect()
|
||||||
|
return
|
||||||
|
|
||||||
|
header = 'XMPP Service Discovery: %s' % self.target_jid
|
||||||
|
print(header)
|
||||||
|
print('-' * len(header))
|
||||||
|
if self.target_node != '':
|
||||||
|
print('Node: %s' % self.target_node)
|
||||||
|
print('-' * len(header))
|
||||||
|
|
||||||
|
if self.get in self.identity_types:
|
||||||
|
print('Identities:')
|
||||||
|
for identity in info['disco_info']['identities']:
|
||||||
|
print(' - ', identity)
|
||||||
|
|
||||||
|
if self.get in self.feature_types:
|
||||||
|
print('Features:')
|
||||||
|
for feature in info['disco_info']['features']:
|
||||||
|
print(' - %s' % feature)
|
||||||
|
|
||||||
|
if self.get in self.items_types:
|
||||||
|
print('Items:')
|
||||||
|
for item in items['disco_items']['items']:
|
||||||
|
print(' - %s' % str(item))
|
||||||
|
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Setup the command line arguments.
|
||||||
|
optp = OptionParser()
|
||||||
|
optp.version = '%%prog 0.1'
|
||||||
|
optp.usage = "Usage: %%prog [options] %s <jid> [<node>]" % \
|
||||||
|
'all|info|items|identities|features'
|
||||||
|
|
||||||
|
optp.add_option('-q','--quiet', help='set logging to ERROR',
|
||||||
|
action='store_const',
|
||||||
|
dest='loglevel',
|
||||||
|
const=logging.ERROR,
|
||||||
|
default=logging.ERROR)
|
||||||
|
optp.add_option('-d','--debug', help='set logging to DEBUG',
|
||||||
|
action='store_const',
|
||||||
|
dest='loglevel',
|
||||||
|
const=logging.DEBUG,
|
||||||
|
default=logging.ERROR)
|
||||||
|
optp.add_option('-v','--verbose', help='set logging to COMM',
|
||||||
|
action='store_const',
|
||||||
|
dest='loglevel',
|
||||||
|
const=5,
|
||||||
|
default=logging.ERROR)
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
opts,args = optp.parse_args()
|
||||||
|
|
||||||
|
# Setup logging.
|
||||||
|
logging.basicConfig(level=opts.loglevel,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
if len(args) < 2:
|
||||||
|
optp.print_help()
|
||||||
|
exit()
|
||||||
|
|
||||||
|
if len(args) == 2:
|
||||||
|
args = (args[0], args[1], '')
|
||||||
|
|
||||||
|
if opts.jid is None:
|
||||||
|
opts.jid = raw_input("Username: ")
|
||||||
|
if opts.password is None:
|
||||||
|
opts.password = getpass.getpass("Password: ")
|
||||||
|
|
||||||
|
# Setup the Disco browser.
|
||||||
|
xmpp = Disco(opts.jid, opts.password, args[1], args[2], args[0])
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
else:
|
||||||
|
print("Unable to connect.")
|
|
@ -118,6 +118,13 @@ if __name__ == '__main__':
|
||||||
xmpp.registerPlugin('xep_0060') # PubSub
|
xmpp.registerPlugin('xep_0060') # PubSub
|
||||||
xmpp.registerPlugin('xep_0199') # XMPP Ping
|
xmpp.registerPlugin('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"
|
||||||
|
|
||||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||||
if xmpp.connect():
|
if xmpp.connect():
|
||||||
# If you do not have the pydns library installed, you will need
|
# If you do not have the pydns library installed, you will need
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -38,13 +38,15 @@ CLASSIFIERS = [ 'Intended Audience :: Developers',
|
||||||
]
|
]
|
||||||
|
|
||||||
packages = [ 'sleekxmpp',
|
packages = [ 'sleekxmpp',
|
||||||
'sleekxmpp/plugins',
|
|
||||||
'sleekxmpp/stanza',
|
'sleekxmpp/stanza',
|
||||||
'sleekxmpp/test',
|
'sleekxmpp/test',
|
||||||
'sleekxmpp/xmlstream',
|
'sleekxmpp/xmlstream',
|
||||||
'sleekxmpp/xmlstream/matcher',
|
'sleekxmpp/xmlstream/matcher',
|
||||||
'sleekxmpp/xmlstream/handler',
|
'sleekxmpp/xmlstream/handler',
|
||||||
'sleekxmpp/thirdparty',
|
'sleekxmpp/thirdparty',
|
||||||
|
'sleekxmpp/plugins',
|
||||||
|
'sleekxmpp/plugins/xep_0030',
|
||||||
|
'sleekxmpp/plugins/xep_0030/stanza'
|
||||||
]
|
]
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
|
|
|
@ -271,7 +271,7 @@ class BaseXMPP(XMLStream):
|
||||||
"""Create a Presence stanza associated with this stream."""
|
"""Create a Presence stanza associated with this stream."""
|
||||||
return Presence(self, *args, **kwargs)
|
return Presence(self, *args, **kwargs)
|
||||||
|
|
||||||
def make_iq(self, id=0, ifrom=None):
|
def make_iq(self, id=0, ifrom=None, ito=None, type=None, query=None):
|
||||||
"""
|
"""
|
||||||
Create a new Iq stanza with a given Id and from JID.
|
Create a new Iq stanza with a given Id and from JID.
|
||||||
|
|
||||||
|
@ -279,11 +279,19 @@ class BaseXMPP(XMLStream):
|
||||||
id -- An ideally unique ID value for this stanza thread.
|
id -- An ideally unique ID value for this stanza thread.
|
||||||
Defaults to 0.
|
Defaults to 0.
|
||||||
ifrom -- The from JID to use for this stanza.
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
type -- The Iq's type, one of: get, set, result, or error.
|
||||||
|
query -- Optional namespace for adding a query element.
|
||||||
"""
|
"""
|
||||||
return self.Iq()._set_stanza_values({'id': str(id),
|
iq = self.Iq()
|
||||||
'from': ifrom})
|
iq['id'] = str(id)
|
||||||
|
iq['to'] = ito
|
||||||
|
iq['from'] = ifrom
|
||||||
|
iq['type'] = itype
|
||||||
|
iq['query'] = query
|
||||||
|
return iq
|
||||||
|
|
||||||
def make_iq_get(self, queryxmlns=None):
|
def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None):
|
||||||
"""
|
"""
|
||||||
Create an Iq stanza of type 'get'.
|
Create an Iq stanza of type 'get'.
|
||||||
|
|
||||||
|
@ -291,21 +299,45 @@ class BaseXMPP(XMLStream):
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
queryxmlns -- The namespace of the query to use.
|
queryxmlns -- The namespace of the query to use.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
iq -- Optionally use an existing stanza instead
|
||||||
|
of generating a new one.
|
||||||
"""
|
"""
|
||||||
return self.Iq()._set_stanza_values({'type': 'get',
|
if not iq:
|
||||||
'query': queryxmlns})
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'get'
|
||||||
|
iq['query'] = queryxmlns
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
return iq
|
||||||
|
|
||||||
def make_iq_result(self, id):
|
def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
|
||||||
"""
|
"""
|
||||||
Create an Iq stanza of type 'result' with the given ID value.
|
Create an Iq stanza of type 'result' with the given ID value.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
id -- An ideally unique ID value. May use self.new_id().
|
id -- An ideally unique ID value. May use self.new_id().
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
iq -- Optionally use an existing stanza instead
|
||||||
|
of generating a new one.
|
||||||
"""
|
"""
|
||||||
return self.Iq()._set_stanza_values({'id': id,
|
if not iq:
|
||||||
'type': 'result'})
|
iq = self.Iq()
|
||||||
|
if id is None:
|
||||||
|
id = self.new_id()
|
||||||
|
iq['id'] = id
|
||||||
|
iq['type'] = 'result'
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
return iq
|
||||||
|
|
||||||
def make_iq_set(self, sub=None):
|
def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None):
|
||||||
"""
|
"""
|
||||||
Create an Iq stanza of type 'set'.
|
Create an Iq stanza of type 'set'.
|
||||||
|
|
||||||
|
@ -313,15 +345,26 @@ class BaseXMPP(XMLStream):
|
||||||
stanza's payload.
|
stanza's payload.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
sub -- A stanza or XML object to use as the Iq's payload.
|
sub -- A stanza or XML object to use as the Iq's payload.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
iq -- Optionally use an existing stanza instead
|
||||||
|
of generating a new one.
|
||||||
"""
|
"""
|
||||||
iq = self.Iq()._set_stanza_values({'type': 'set'})
|
if not iq:
|
||||||
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
if sub != None:
|
if sub != None:
|
||||||
iq.append(sub)
|
iq.append(sub)
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
return iq
|
return iq
|
||||||
|
|
||||||
def make_iq_error(self, id, type='cancel',
|
def make_iq_error(self, id, type='cancel',
|
||||||
condition='feature-not-implemented', text=None):
|
condition='feature-not-implemented',
|
||||||
|
text=None, ito=None, ifrom=None, iq=None):
|
||||||
"""
|
"""
|
||||||
Create an Iq stanza of type 'error'.
|
Create an Iq stanza of type 'error'.
|
||||||
|
|
||||||
|
@ -332,14 +375,24 @@ class BaseXMPP(XMLStream):
|
||||||
condition -- The error condition.
|
condition -- The error condition.
|
||||||
Defaults to 'feature-not-implemented'.
|
Defaults to 'feature-not-implemented'.
|
||||||
text -- A message describing the cause of the error.
|
text -- A message describing the cause of the error.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
|
iq -- Optionally use an existing stanza instead
|
||||||
|
of generating a new one.
|
||||||
"""
|
"""
|
||||||
iq = self.Iq()._set_stanza_values({'id': id})
|
if not iq:
|
||||||
iq['error']._set_stanza_values({'type': type,
|
iq = self.Iq()
|
||||||
'condition': condition,
|
iq['id'] = id
|
||||||
'text': text})
|
iq['error']['type'] = type
|
||||||
|
iq['error']['condition'] = condition
|
||||||
|
iq['error']['text'] = text
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
return iq
|
return iq
|
||||||
|
|
||||||
def make_iq_query(self, iq=None, xmlns=''):
|
def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
|
||||||
"""
|
"""
|
||||||
Create or modify an Iq stanza to use the given
|
Create or modify an Iq stanza to use the given
|
||||||
query namespace.
|
query namespace.
|
||||||
|
@ -348,10 +401,16 @@ class BaseXMPP(XMLStream):
|
||||||
iq -- Optional Iq stanza to modify. A new
|
iq -- Optional Iq stanza to modify. A new
|
||||||
stanza is created otherwise.
|
stanza is created otherwise.
|
||||||
xmlns -- The query's namespace.
|
xmlns -- The query's namespace.
|
||||||
|
ito -- The destination JID for this stanza.
|
||||||
|
ifrom -- The from JID to use for this stanza.
|
||||||
"""
|
"""
|
||||||
if not iq:
|
if not iq:
|
||||||
iq = self.Iq()
|
iq = self.Iq()
|
||||||
iq['query'] = xmlns
|
iq['query'] = xmlns
|
||||||
|
if ito:
|
||||||
|
iq['to'] = ito
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
return iq
|
return iq
|
||||||
|
|
||||||
def make_query_roster(self, iq=None):
|
def make_query_roster(self, iq=None):
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
|
|
||||||
See the file LICENSE for copying permission.
|
See the file LICENSE for copying permission.
|
||||||
"""
|
"""
|
||||||
__all__ = ['xep_0004', 'xep_0012', 'xep_0030', 'xep_0033', 'xep_0045',
|
__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
|
||||||
'xep_0050', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify',
|
'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086',
|
||||||
'xep_0060', 'xep_0202']
|
'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify']
|
||||||
|
|
|
@ -143,7 +143,7 @@ class gmail_notify(base.base_plugin):
|
||||||
log.info('Gmail: Searching for emails matching: "%s"' % query)
|
log.info('Gmail: Searching for emails matching: "%s"' % query)
|
||||||
iq = self.xmpp.Iq()
|
iq = self.xmpp.Iq()
|
||||||
iq['type'] = 'get'
|
iq['type'] = 'get'
|
||||||
iq['to'] = self.xmpp.jid
|
iq['to'] = self.xmpp.boundjid.bare
|
||||||
iq['gmail']['q'] = query
|
iq['gmail']['q'] = query
|
||||||
iq['gmail']['newer-than-time'] = newer
|
iq['gmail']['newer-than-time'] = newer
|
||||||
return iq.send()
|
return iq.send()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from . import base
|
from . import base
|
||||||
import logging
|
import logging
|
||||||
from xml.etree import cElementTree as ET
|
from xml.etree import cElementTree as ET
|
||||||
import types
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -43,7 +42,7 @@ class jobs(base.base_plugin):
|
||||||
iq['psstate']['item'] = jobid
|
iq['psstate']['item'] = jobid
|
||||||
iq['psstate']['payload'] = state
|
iq['psstate']['payload'] = state
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or type(result) == types.BooleanType or result['type'] != 'result':
|
if result is None or type(result) == bool or result['type'] != 'result':
|
||||||
log.error("Unable to change %s:%s to %s" % (node, jobid, state))
|
log.error("Unable to change %s:%s to %s" % (node, jobid, state))
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -13,7 +13,6 @@ from .. xmlstream.handler.callback import Callback
|
||||||
from .. xmlstream.matcher.xpath import MatchXPath
|
from .. xmlstream.matcher.xpath import MatchXPath
|
||||||
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
|
||||||
from .. stanza.message import Message
|
from .. stanza.message import Message
|
||||||
import types
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -203,7 +202,7 @@ class Form(ElementBase):
|
||||||
|
|
||||||
def merge(self, other):
|
def merge(self, other):
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
if type(other) == types.DictType:
|
if type(other) == dict:
|
||||||
new.setValues(other)
|
new.setValues(other)
|
||||||
return new
|
return new
|
||||||
nfields = new.getFields(use_dict=True)
|
nfields = new.getFields(use_dict=True)
|
||||||
|
|
|
@ -26,30 +26,66 @@ class xep_0030(base_plugin):
|
||||||
"""
|
"""
|
||||||
XEP-0030: Service Discovery
|
XEP-0030: Service Discovery
|
||||||
|
|
||||||
|
Service discovery in XMPP allows entities to discover information about
|
||||||
|
other agents in the network, such as the feature sets supported by a
|
||||||
|
client, or signposts to other, related entities.
|
||||||
|
|
||||||
|
Also see <http://www.xmpp.org/extensions/xep-0030.html>.
|
||||||
|
|
||||||
|
The XEP-0030 plugin works using a hierarchy of dynamic
|
||||||
|
node handlers, ranging from global handlers to specific
|
||||||
|
JID+node handlers. The default set of handlers operate
|
||||||
|
in a static manner, storing disco information in memory.
|
||||||
|
However, custom handlers may use any available backend
|
||||||
|
storage mechanism desired, such as SQLite or Redis.
|
||||||
|
|
||||||
|
Node handler hierarchy:
|
||||||
|
JID | Node | Level
|
||||||
|
---------------------
|
||||||
|
None | None | Global
|
||||||
|
Given | None | All nodes for the JID
|
||||||
|
None | Given | Node on self.xmpp.boundjid
|
||||||
|
Given | Given | A single node
|
||||||
|
|
||||||
Stream Handlers:
|
Stream Handlers:
|
||||||
Disco Info --
|
Disco Info -- Any Iq stanze that includes a query with the
|
||||||
Disco Items --
|
namespace http://jabber.org/protocol/disco#info.
|
||||||
|
Disco Items -- Any Iq stanze that includes a query with the
|
||||||
|
namespace http://jabber.org/protocol/disco#items.
|
||||||
|
|
||||||
Events:
|
Events:
|
||||||
disco_info --
|
disco_info -- Received a disco#info Iq query result.
|
||||||
disco_items --
|
disco_items -- Received a disco#items Iq query result.
|
||||||
disco_info_query --
|
disco_info_query -- Received a disco#info Iq query request.
|
||||||
disco_items_query --
|
disco_items_query -- Received a disco#items Iq query request.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
stanza -- A reference to the module containing the stanza classes
|
||||||
|
provided by this plugin.
|
||||||
|
static -- Object containing the default set of static node handlers.
|
||||||
|
xmpp -- The main SleekXMPP object.
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
set_node_handler --
|
set_node_handler -- Assign a handler to a JID/node combination.
|
||||||
del_node_handler --
|
del_node_handler -- Remove a handler from a JID/node combination.
|
||||||
add_identity --
|
get_info -- Retrieve disco#info data, locally or remote.
|
||||||
|
get_items -- Retrieve disco#items data, locally or remote.
|
||||||
|
set_identities --
|
||||||
|
set_features --
|
||||||
|
set_items --
|
||||||
|
del_items --
|
||||||
del_identity --
|
del_identity --
|
||||||
add_feature --
|
|
||||||
del_feature --
|
del_feature --
|
||||||
add_item --
|
|
||||||
del_item --
|
del_item --
|
||||||
get_info --
|
add_identity --
|
||||||
get_items --
|
add_feature --
|
||||||
|
add_item --
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def plugin_init(self):
|
def plugin_init(self):
|
||||||
|
"""
|
||||||
|
Start the XEP-0030 plugin.
|
||||||
|
"""
|
||||||
self.xep = '0030'
|
self.xep = '0030'
|
||||||
self.description = 'Service Discovery'
|
self.description = 'Service Discovery'
|
||||||
self.stanza = sleekxmpp.plugins.xep_0030.stanza
|
self.stanza = sleekxmpp.plugins.xep_0030.stanza
|
||||||
|
@ -70,42 +106,89 @@ class xep_0030(base_plugin):
|
||||||
self.static = StaticDisco(self.xmpp)
|
self.static = StaticDisco(self.xmpp)
|
||||||
|
|
||||||
self._disco_ops = ['get_info', 'set_identities', 'set_features',
|
self._disco_ops = ['get_info', 'set_identities', 'set_features',
|
||||||
'del_info', 'get_items', 'set_items', 'del_items',
|
'get_items', 'set_items', 'del_items',
|
||||||
'add_identity', 'del_identity', 'add_feature',
|
'add_identity', 'del_identity', 'add_feature',
|
||||||
'del_feature', 'add_item', 'del_item']
|
'del_feature', 'add_item', 'del_item']
|
||||||
self.handlers = {}
|
self._handlers = {}
|
||||||
for op in self._disco_ops:
|
for op in self._disco_ops:
|
||||||
self.handlers[op] = {'global': getattr(self.static, op),
|
self._handlers[op] = {'global': getattr(self.static, op),
|
||||||
'jid': {},
|
'jid': {},
|
||||||
'node': {}}
|
'node': {}}
|
||||||
|
|
||||||
|
|
||||||
def set_node_handler(self, htype, jid=None, node=None, handler=None):
|
def set_node_handler(self, htype, jid=None, node=None, handler=None):
|
||||||
"""
|
"""
|
||||||
|
Add a node handler for the given hierarchy level and
|
||||||
|
handler type.
|
||||||
|
|
||||||
|
Node handlers are ordered in a hierarchy where the
|
||||||
|
most specific handler is executed. Thus, a fallback,
|
||||||
|
global handler can be used for the majority of cases
|
||||||
|
with a few node specific handler that override the
|
||||||
|
global behavior.
|
||||||
|
|
||||||
|
Node handler hierarchy:
|
||||||
|
JID | Node | Level
|
||||||
|
---------------------
|
||||||
|
None | None | Global
|
||||||
|
Given | None | All nodes for the JID
|
||||||
|
None | Given | Node on self.xmpp.boundjid
|
||||||
|
Given | Given | A single node
|
||||||
|
|
||||||
|
Handler types:
|
||||||
|
get_info
|
||||||
|
get_items
|
||||||
|
set_identities
|
||||||
|
set_features
|
||||||
|
set_items
|
||||||
|
del_items
|
||||||
|
del_identity
|
||||||
|
del_feature
|
||||||
|
del_item
|
||||||
|
add_identity
|
||||||
|
add_feature
|
||||||
|
add_item
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
htype
|
htype -- The operation provided by the handler.
|
||||||
jid
|
jid -- The JID the handler applies to. May be narrowed
|
||||||
node
|
further if a node is given.
|
||||||
handler
|
node -- The particular node the handler is for. If no JID
|
||||||
|
is given, then the self.xmpp.boundjid.full is
|
||||||
|
assumed.
|
||||||
|
handler -- The handler function to use.
|
||||||
"""
|
"""
|
||||||
if htype not in self._disco_ops:
|
if htype not in self._disco_ops:
|
||||||
return
|
return
|
||||||
if jid is None and node is None:
|
if jid is None and node is None:
|
||||||
self.handlers[htype]['global'] = handler
|
self._handlers[htype]['global'] = handler
|
||||||
elif node is None:
|
elif node is None:
|
||||||
self.handlers[htype]['jid'][jid] = handler
|
self._handlers[htype]['jid'][jid] = handler
|
||||||
elif jid is None:
|
elif jid is None:
|
||||||
jid = self.xmpp.boundjid.full
|
jid = self.xmpp.boundjid.full
|
||||||
self.handlers[htype]['node'][(jid, node)] = handler
|
self._handlers[htype]['node'][(jid, node)] = handler
|
||||||
else:
|
else:
|
||||||
self.handlers[htype]['node'][(jid, node)] = handler
|
self._handlers[htype]['node'][(jid, node)] = handler
|
||||||
|
|
||||||
def del_node_handler(self, htype, jid, node):
|
def del_node_handler(self, htype, jid, node):
|
||||||
"""
|
"""
|
||||||
|
Remove a handler type for a JID and node combination.
|
||||||
|
|
||||||
|
The next handler in the hierarchy will be used if one
|
||||||
|
exists. If removing the global handler, make sure that
|
||||||
|
other handlers exist to process existing nodes.
|
||||||
|
|
||||||
|
Node handler hierarchy:
|
||||||
|
JID | Node | Level
|
||||||
|
---------------------
|
||||||
|
None | None | Global
|
||||||
|
Given | None | All nodes for the JID
|
||||||
|
None | Given | Node on self.xmpp.boundjid
|
||||||
|
Given | Given | A single node
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
htype
|
htype -- The type of handler to remove.
|
||||||
jid
|
jid -- The JID from which to remove the handler.
|
||||||
node
|
node -- The node from which to remove the handler.
|
||||||
"""
|
"""
|
||||||
self.set_node_handler(htype, jid, node, None)
|
self.set_node_handler(htype, jid, node, None)
|
||||||
|
|
||||||
|
@ -132,14 +215,28 @@ class xep_0030(base_plugin):
|
||||||
|
|
||||||
def get_info(self, jid=None, node=None, local=False, **kwargs):
|
def get_info(self, jid=None, node=None, local=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
Retrieve the disco#info results from a given JID/node combination.
|
||||||
|
|
||||||
|
Info may be retrieved from both local resources and remote agents;
|
||||||
|
the local parameter indicates if the information should be gathered
|
||||||
|
by executing the local node handlers, or if a disco#info stanza
|
||||||
|
must be generated and sent.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
jid --
|
jid -- Request info from this JID.
|
||||||
node --
|
node -- The particular node to query.
|
||||||
local --
|
local -- If true, then the query is for a JID/node
|
||||||
dfrom --
|
combination handled by this Sleek instance and
|
||||||
block --
|
no stanzas need to be sent.
|
||||||
timeout --
|
Otherwise, a disco stanza must be sent to the
|
||||||
callback --
|
remove JID to retrieve the info.
|
||||||
|
dfrom -- 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.
|
||||||
"""
|
"""
|
||||||
if local or jid is None:
|
if local or jid is None:
|
||||||
log.debug("Looking up local disco#info data " + \
|
log.debug("Looking up local disco#info data " + \
|
||||||
|
@ -158,14 +255,28 @@ class xep_0030(base_plugin):
|
||||||
|
|
||||||
def get_items(self, jid=None, node=None, local=False, **kwargs):
|
def get_items(self, jid=None, node=None, local=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
Retrieve the disco#items results from a given JID/node combination.
|
||||||
|
|
||||||
|
Items may be retrieved from both local resources and remote agents;
|
||||||
|
the local parameter indicates if the items should be gathered by
|
||||||
|
executing the local node handlers, or if a disco#items stanza must
|
||||||
|
be generated and sent.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
jid --
|
jid -- Request info from this JID.
|
||||||
node --
|
node -- The particular node to query.
|
||||||
local --
|
local -- If true, then the query is for a JID/node
|
||||||
dfrom --
|
combination handled by this Sleek instance and
|
||||||
block --
|
no stanzas need to be sent.
|
||||||
timeout --
|
Otherwise, a disco stanza must be sent to the
|
||||||
callback --
|
remove JID to retrieve the items.
|
||||||
|
dfrom -- 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.
|
||||||
"""
|
"""
|
||||||
if local or jid is None:
|
if local or jid is None:
|
||||||
return self._run_node_handler('get_items', jid, node, kwargs)
|
return self._run_node_handler('get_items', jid, node, kwargs)
|
||||||
|
@ -179,37 +290,169 @@ class xep_0030(base_plugin):
|
||||||
block=kwargs.get('block', None),
|
block=kwargs.get('block', None),
|
||||||
callback=kwargs.get('callback', None))
|
callback=kwargs.get('callback', None))
|
||||||
|
|
||||||
def set_info(self, jid=None, node=None, **kwargs):
|
|
||||||
self._run_node_handler('set_info', jid, node, kwargs)
|
|
||||||
|
|
||||||
def del_info(self, jid=None, node=None, **kwargs):
|
|
||||||
self._run_node_handler('del_info', jid, node, kwargs)
|
|
||||||
|
|
||||||
def set_items(self, jid=None, node=None, **kwargs):
|
def set_items(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Set or replace all items for the specified JID/node combination.
|
||||||
|
|
||||||
|
The given items must be in a list or set where each item is a
|
||||||
|
tuple of the form: (jid, node, name).
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- Optional node to modify.
|
||||||
|
items -- A series of items in tuple format.
|
||||||
|
"""
|
||||||
self._run_node_handler('set_items', jid, node, kwargs)
|
self._run_node_handler('set_items', jid, node, kwargs)
|
||||||
|
|
||||||
def del_items(self, jid=None, node=None, **kwargs):
|
def del_items(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove all items from the given JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- Optional node to modify.
|
||||||
|
"""
|
||||||
self._run_node_handler('del_items', jid, node, kwargs)
|
self._run_node_handler('del_items', jid, node, kwargs)
|
||||||
|
|
||||||
def add_identity(self, jid=None, node=None, **kwargs):
|
|
||||||
self._run_node_handler('add_identity', jid, node, kwargs)
|
|
||||||
|
|
||||||
def add_feature(self, jid=None, node=None, **kwargs):
|
|
||||||
self._run_node_handler('add_feature', jid, node, kwargs)
|
|
||||||
|
|
||||||
def del_identity(self, jid=None, node=None, **kwargs):
|
|
||||||
self._run_node_handler('del_identity', jid, node, kwargs)
|
|
||||||
|
|
||||||
def del_feature(self, jid=None, node=None, **kwargs):
|
|
||||||
self._run_node_handler('del_feature', jid, node, kwargs)
|
|
||||||
|
|
||||||
def add_item(self, jid=None, node=None, **kwargs):
|
def add_item(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Add a new item element to the given JID/node combination.
|
||||||
|
|
||||||
|
Each item is required to have a JID, but may also specify
|
||||||
|
a node value to reference non-addressable entities.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
ijid -- The JID for the item.
|
||||||
|
inode -- Optional node for the item.
|
||||||
|
name -- Optional name for the item.
|
||||||
|
"""
|
||||||
self._run_node_handler('add_item', jid, node, kwargs)
|
self._run_node_handler('add_item', jid, node, kwargs)
|
||||||
|
|
||||||
def del_item(self, jid=None, node=None, **kwargs):
|
def del_item(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove a single item from the given JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
ijid -- The item's JID.
|
||||||
|
inode -- The item's node.
|
||||||
|
"""
|
||||||
self._run_node_handler('del_item', jid, node, kwargs)
|
self._run_node_handler('del_item', jid, node, kwargs)
|
||||||
|
|
||||||
def _run_node_handler(self, htype, jid, node, data=None):
|
def add_identity(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Add a new identity to the given JID/node combination.
|
||||||
|
|
||||||
|
Each identity must be unique in terms of all four identity
|
||||||
|
components: category, type, name, and language.
|
||||||
|
|
||||||
|
Multiple, identical category/type pairs are allowed only
|
||||||
|
if the xml:lang values are different. Likewise, multiple
|
||||||
|
category/type/xml:lang pairs are allowed so long as the
|
||||||
|
names are different. A category and type is always required.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
category -- The identity's category.
|
||||||
|
itype -- The identity's type.
|
||||||
|
name -- Optional name for the identity.
|
||||||
|
lang -- Optional two-letter language code.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('add_identity', jid, node, kwargs)
|
||||||
|
|
||||||
|
def add_feature(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Add a feature to a JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
feature -- The namespace of the supported feature.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('add_feature', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_identity(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove an identity from the given JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
category -- The identity's category.
|
||||||
|
itype -- The identity's type value.
|
||||||
|
name -- Optional, human readable name for the identity.
|
||||||
|
lang -- Optional, the identity's xml:lang value.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_identity', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_feature(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove a feature from a given JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
feature -- The feature's namespace.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_feature', jid, node, kwargs)
|
||||||
|
|
||||||
|
def set_identities(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Add or replace all identities for the given JID/node combination.
|
||||||
|
|
||||||
|
The identities must be in a set where each identity is a tuple
|
||||||
|
of the form: (category, type, lang, name)
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
identities -- A set of identities in tuple form.
|
||||||
|
lang -- Optional, xml:lang value.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('set_identities', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_identities(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove all identities for a JID/node combination.
|
||||||
|
|
||||||
|
If a language is specified, only identities using that
|
||||||
|
language will be removed.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
lang -- Optional. If given, only remove identities
|
||||||
|
using this xml:lang value.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_identities', jid, node, kwargs)
|
||||||
|
|
||||||
|
def set_features(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Add or replace the set of supported features
|
||||||
|
for a JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
features -- The new set of supported features.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('set_features', jid, node, kwargs)
|
||||||
|
|
||||||
|
def del_features(self, jid=None, node=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove all features from a JID/node combination.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to modify.
|
||||||
|
node -- The node to modify.
|
||||||
|
"""
|
||||||
|
self._run_node_handler('del_features', jid, node, kwargs)
|
||||||
|
|
||||||
|
def _run_node_handler(self, htype, jid, node, data={}):
|
||||||
"""
|
"""
|
||||||
Execute the most specific node handler for the given
|
Execute the most specific node handler for the given
|
||||||
JID/node combination.
|
JID/node combination.
|
||||||
|
@ -218,19 +461,19 @@ class xep_0030(base_plugin):
|
||||||
htype -- The handler type to execute.
|
htype -- The handler type to execute.
|
||||||
jid -- The JID requested.
|
jid -- The JID requested.
|
||||||
node -- The node requested.
|
node -- The node requested.
|
||||||
dat -- Optional, custom data to pass to the handler.
|
data -- Optional, custom data to pass to the handler.
|
||||||
"""
|
"""
|
||||||
if jid is None:
|
if jid is None:
|
||||||
jid = self.xmpp.boundjid.full
|
jid = self.xmpp.boundjid.full
|
||||||
if node is None:
|
if node is None:
|
||||||
node = ''
|
node = ''
|
||||||
|
|
||||||
if self.handlers[htype]['node'].get((jid, node), False):
|
if self._handlers[htype]['node'].get((jid, node), False):
|
||||||
return self.handlers[htype]['node'][(jid, node)](jid, node, data)
|
return self._handlers[htype]['node'][(jid, node)](jid, node, data)
|
||||||
elif self.handlers[htype]['jid'].get(jid, False):
|
elif self._handlers[htype]['jid'].get(jid, False):
|
||||||
return self.handlers[htype]['jid'][jid](jid, node, data)
|
return self._handlers[htype]['jid'][jid](jid, node, data)
|
||||||
elif self.handlers[htype]['global']:
|
elif self._handlers[htype]['global']:
|
||||||
return self.handlers[htype]['global'](jid, node, data)
|
return self._handlers[htype]['global'](jid, node, data)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -311,4 +554,3 @@ class xep_0030(base_plugin):
|
||||||
"Using default disco#info feature.")
|
"Using default disco#info feature.")
|
||||||
info.add_feature(info.namespace)
|
info.add_feature(info.namespace)
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,6 @@ from sleekxmpp.xmlstream import ElementBase, ET
|
||||||
class DiscoItems(ElementBase):
|
class DiscoItems(ElementBase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
Example disco#items stanzas:
|
Example disco#items stanzas:
|
||||||
<iq type="get">
|
<iq type="get">
|
||||||
<query xmlns="http://jabber.org/protocol/disco#items" />
|
<query xmlns="http://jabber.org/protocol/disco#items" />
|
||||||
|
|
|
@ -35,6 +35,11 @@ class StaticDisco(object):
|
||||||
|
|
||||||
def __init__(self, xmpp):
|
def __init__(self, xmpp):
|
||||||
"""
|
"""
|
||||||
|
Create a static disco interface. Sets of disco#info and
|
||||||
|
disco#items are maintained for every given JID and node
|
||||||
|
combination. These stanzas are used to store disco
|
||||||
|
information in memory without any additional processing.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
xmpp -- The main SleekXMPP object.
|
xmpp -- The main SleekXMPP object.
|
||||||
"""
|
"""
|
||||||
|
@ -52,7 +57,7 @@ class StaticDisco(object):
|
||||||
self.nodes[(jid, node)]['info']['node'] = node
|
self.nodes[(jid, node)]['info']['node'] = node
|
||||||
self.nodes[(jid, node)]['items']['node'] = node
|
self.nodes[(jid, node)]['items']['node'] = node
|
||||||
|
|
||||||
def get_info(self, jid, node, data=None):
|
def get_info(self, jid, node, data):
|
||||||
if (jid, node) not in self.nodes:
|
if (jid, node) not in self.nodes:
|
||||||
if not node:
|
if not node:
|
||||||
return DiscoInfo()
|
return DiscoInfo()
|
||||||
|
@ -61,11 +66,11 @@ class StaticDisco(object):
|
||||||
else:
|
else:
|
||||||
return self.nodes[(jid, node)]['info']
|
return self.nodes[(jid, node)]['info']
|
||||||
|
|
||||||
def del_info(self, jid, node, data=None):
|
def del_info(self, jid, node, data):
|
||||||
if (jid, node) in self.nodes:
|
if (jid, node) in self.nodes:
|
||||||
self.nodes[(jid, node)]['info'] = DiscoInfo()
|
self.nodes[(jid, node)]['info'] = DiscoInfo()
|
||||||
|
|
||||||
def get_items(self, jid, node, data=None):
|
def get_items(self, jid, node, data):
|
||||||
if (jid, node) not in self.nodes:
|
if (jid, node) not in self.nodes:
|
||||||
if not node:
|
if not node:
|
||||||
return DiscoInfo()
|
return DiscoInfo()
|
||||||
|
@ -74,14 +79,16 @@ class StaticDisco(object):
|
||||||
else:
|
else:
|
||||||
return self.nodes[(jid, node)]['items']
|
return self.nodes[(jid, node)]['items']
|
||||||
|
|
||||||
def set_items(self, jid, node, data=None):
|
def set_items(self, jid, node, data):
|
||||||
pass
|
items = data.get('items', set())
|
||||||
|
self.add_node(jid, node)
|
||||||
|
self.nodes[(jid, node)]['items']['items'] = items
|
||||||
|
|
||||||
def del_items(self, jid, node, data=None):
|
def del_items(self, jid, node, data):
|
||||||
if (jid, node) in self.nodes:
|
if (jid, node) in self.nodes:
|
||||||
self.nodes[(jid, node)]['items'] = DiscoItems()
|
self.nodes[(jid, node)]['items'] = DiscoItems()
|
||||||
|
|
||||||
def add_identity(self, jid, node, data={}):
|
def add_identity(self, jid, node, data):
|
||||||
self.add_node(jid, node)
|
self.add_node(jid, node)
|
||||||
self.nodes[(jid, node)]['info'].add_identity(
|
self.nodes[(jid, node)]['info'].add_identity(
|
||||||
data.get('category', ''),
|
data.get('category', ''),
|
||||||
|
@ -89,10 +96,12 @@ class StaticDisco(object):
|
||||||
data.get('name', None),
|
data.get('name', None),
|
||||||
data.get('lang', None))
|
data.get('lang', None))
|
||||||
|
|
||||||
def set_identities(self, jid, node, data=None):
|
def set_identities(self, jid, node, data):
|
||||||
pass
|
identities = data.get('identities', set())
|
||||||
|
self.add_node(jid, node)
|
||||||
|
self.nodes[(jid, node)]['info']['identities'] = identities
|
||||||
|
|
||||||
def del_identity(self, jid, node, data=None):
|
def del_identity(self, jid, node, data):
|
||||||
if (jid, node) not in self.nodes:
|
if (jid, node) not in self.nodes:
|
||||||
return
|
return
|
||||||
self.nodes[(jid, node)]['info'].del_identity(
|
self.nodes[(jid, node)]['info'].del_identity(
|
||||||
|
@ -101,27 +110,29 @@ class StaticDisco(object):
|
||||||
data.get('name', None),
|
data.get('name', None),
|
||||||
data.get('lang', None))
|
data.get('lang', None))
|
||||||
|
|
||||||
|
def add_feature(self, jid, node, data):
|
||||||
def add_feature(self, jid, node, data=None):
|
|
||||||
self.add_node(jid, node)
|
self.add_node(jid, node)
|
||||||
self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
|
self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
|
||||||
|
|
||||||
def set_features(self, jid, node, data=None):
|
def set_features(self, jid, node, data):
|
||||||
pass
|
features = data.get('features', set())
|
||||||
|
self.add_node(jid, node)
|
||||||
|
self.nodes[(jid, node)]['info']['features'] = features
|
||||||
|
|
||||||
def del_feature(self, jid, node, data=None):
|
def del_feature(self, jid, node, data):
|
||||||
if (jid, node) not in self.nodes:
|
if (jid, node) not in self.nodes:
|
||||||
return
|
return
|
||||||
self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
|
self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
|
||||||
|
|
||||||
def add_item(self, jid, node, data=None):
|
def add_item(self, jid, node, data):
|
||||||
self.add_node(jid, node)
|
self.add_node(jid, node)
|
||||||
self.nodes[(jid, node)]['items'].add_item(
|
self.nodes[(jid, node)]['items'].add_item(
|
||||||
data.get('ijid', ''),
|
data.get('ijid', ''),
|
||||||
node=data.get('inode', None),
|
node=data.get('inode', None),
|
||||||
name=data.get('name', None))
|
name=data.get('name', None))
|
||||||
|
|
||||||
def del_item(self, jid, node, data=None):
|
def del_item(self, jid, node, data):
|
||||||
if (jid, node) in self.nodes:
|
if (jid, node) in self.nodes:
|
||||||
self.nodes[(jid, node)]['items'].del_item(**data)
|
self.nodes[(jid, node)]['items'].del_item(
|
||||||
|
data.get('ijid', ''),
|
||||||
|
node=data.get('inode', None))
|
||||||
|
|
|
@ -20,325 +20,333 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MUCPresence(ElementBase):
|
class MUCPresence(ElementBase):
|
||||||
name = 'x'
|
name = 'x'
|
||||||
namespace = 'http://jabber.org/protocol/muc#user'
|
namespace = 'http://jabber.org/protocol/muc#user'
|
||||||
plugin_attrib = 'muc'
|
plugin_attrib = 'muc'
|
||||||
interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
|
interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
|
||||||
affiliations = set(('', ))
|
affiliations = set(('', ))
|
||||||
roles = set(('', ))
|
roles = set(('', ))
|
||||||
|
|
||||||
def getXMLItem(self):
|
def getXMLItem(self):
|
||||||
item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
|
item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
|
||||||
if item is None:
|
if item is None:
|
||||||
item = ET.Element('{http://jabber.org/protocol/muc#user}item')
|
item = ET.Element('{http://jabber.org/protocol/muc#user}item')
|
||||||
self.xml.append(item)
|
self.xml.append(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def getAffiliation(self):
|
def getAffiliation(self):
|
||||||
#TODO if no affilation, set it to the default and return default
|
#TODO if no affilation, set it to the default and return default
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
return item.get('affiliation', '')
|
return item.get('affiliation', '')
|
||||||
|
|
||||||
def setAffiliation(self, value):
|
def setAffiliation(self, value):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO check for valid affiliation
|
#TODO check for valid affiliation
|
||||||
item.attrib['affiliation'] = value
|
item.attrib['affiliation'] = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delAffiliation(self):
|
def delAffiliation(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO set default affiliation
|
#TODO set default affiliation
|
||||||
if 'affiliation' in item.attrib: del item.attrib['affiliation']
|
if 'affiliation' in item.attrib: del item.attrib['affiliation']
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def getJid(self):
|
def getJid(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
return JID(item.get('jid', ''))
|
return JID(item.get('jid', ''))
|
||||||
|
|
||||||
def setJid(self, value):
|
def setJid(self, value):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
value = str(value)
|
value = str(value)
|
||||||
item.attrib['jid'] = value
|
item.attrib['jid'] = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delJid(self):
|
def delJid(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
if 'jid' in item.attrib: del item.attrib['jid']
|
if 'jid' in item.attrib: del item.attrib['jid']
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def getRole(self):
|
def getRole(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO get default role, set default role if none
|
#TODO get default role, set default role if none
|
||||||
return item.get('role', '')
|
return item.get('role', '')
|
||||||
|
|
||||||
def setRole(self, value):
|
def setRole(self, value):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO check for valid role
|
#TODO check for valid role
|
||||||
item.attrib['role'] = value
|
item.attrib['role'] = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delRole(self):
|
def delRole(self):
|
||||||
item = self.getXMLItem()
|
item = self.getXMLItem()
|
||||||
#TODO set default role
|
#TODO set default role
|
||||||
if 'role' in item.attrib: del item.attrib['role']
|
if 'role' in item.attrib: del item.attrib['role']
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def getNick(self):
|
def getNick(self):
|
||||||
return self.parent()['from'].resource
|
return self.parent()['from'].resource
|
||||||
|
|
||||||
def getRoom(self):
|
def getRoom(self):
|
||||||
return self.parent()['from'].bare
|
return self.parent()['from'].bare
|
||||||
|
|
||||||
def setNick(self, value):
|
def setNick(self, value):
|
||||||
log.warning("Cannot set nick through mucpresence plugin.")
|
log.warning("Cannot set nick through mucpresence plugin.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def setRoom(self, value):
|
def setRoom(self, value):
|
||||||
log.warning("Cannot set room through mucpresence plugin.")
|
log.warning("Cannot set room through mucpresence plugin.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delNick(self):
|
def delNick(self):
|
||||||
log.warning("Cannot delete nick through mucpresence plugin.")
|
log.warning("Cannot delete nick through mucpresence plugin.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def delRoom(self):
|
def delRoom(self):
|
||||||
log.warning("Cannot delete room through mucpresence plugin.")
|
log.warning("Cannot delete room through mucpresence plugin.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
class xep_0045(base.base_plugin):
|
class xep_0045(base.base_plugin):
|
||||||
"""
|
"""
|
||||||
Impliments XEP-0045 Multi User Chat
|
Implements XEP-0045 Multi User Chat
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def plugin_init(self):
|
def plugin_init(self):
|
||||||
self.rooms = {}
|
self.rooms = {}
|
||||||
self.ourNicks = {}
|
self.ourNicks = {}
|
||||||
self.xep = '0045'
|
self.xep = '0045'
|
||||||
self.description = 'Multi User Chat'
|
self.description = 'Multi User Chat'
|
||||||
# load MUC support in presence stanzas
|
# load MUC support in presence stanzas
|
||||||
registerStanzaPlugin(Presence, MUCPresence)
|
registerStanzaPlugin(Presence, MUCPresence)
|
||||||
self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
|
self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
|
||||||
self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
|
self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
|
||||||
self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
|
self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
|
||||||
|
self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{http://jabber.org/protocol/muc#user}x/invite" % self.xmpp.default_ns), self.handle_groupchat_invite))
|
||||||
|
|
||||||
def handle_groupchat_presence(self, pr):
|
def handle_groupchat_invite(self, inv):
|
||||||
""" Handle a presence in a muc.
|
""" Handle an invite into a muc.
|
||||||
"""
|
"""
|
||||||
got_offline = False
|
logging.debug("MUC invite to %s from %s: %s" % (inv['from'], inv["from"], inv))
|
||||||
got_online = False
|
if inv['from'] not in self.rooms.keys():
|
||||||
if pr['muc']['room'] not in self.rooms.keys():
|
self.xmpp.event("groupchat_invite", inv)
|
||||||
return
|
|
||||||
entry = pr['muc'].getStanzaValues()
|
|
||||||
entry['show'] = pr['show']
|
|
||||||
entry['status'] = pr['status']
|
|
||||||
if pr['type'] == 'unavailable':
|
|
||||||
if entry['nick'] in self.rooms[entry['room']]:
|
|
||||||
del self.rooms[entry['room']][entry['nick']]
|
|
||||||
got_offline = True
|
|
||||||
else:
|
|
||||||
if entry['nick'] not in self.rooms[entry['room']]:
|
|
||||||
got_online = True
|
|
||||||
self.rooms[entry['room']][entry['nick']] = entry
|
|
||||||
log.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
|
|
||||||
self.xmpp.event("groupchat_presence", pr)
|
|
||||||
self.xmpp.event("muc::%s::presence" % entry['room'], pr)
|
|
||||||
if got_offline:
|
|
||||||
self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
|
|
||||||
if got_online:
|
|
||||||
self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
|
|
||||||
|
|
||||||
def handle_groupchat_message(self, msg):
|
def handle_groupchat_presence(self, pr):
|
||||||
""" Handle a message event in a muc.
|
""" Handle a presence in a muc.
|
||||||
"""
|
"""
|
||||||
self.xmpp.event('groupchat_message', msg)
|
got_offline = False
|
||||||
self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
|
got_online = False
|
||||||
|
if pr['muc']['room'] not in self.rooms.keys():
|
||||||
|
return
|
||||||
|
entry = pr['muc'].getStanzaValues()
|
||||||
|
entry['show'] = pr['show']
|
||||||
|
entry['status'] = pr['status']
|
||||||
|
if pr['type'] == 'unavailable':
|
||||||
|
if entry['nick'] in self.rooms[entry['room']]:
|
||||||
|
del self.rooms[entry['room']][entry['nick']]
|
||||||
|
got_offline = True
|
||||||
|
else:
|
||||||
|
if entry['nick'] not in self.rooms[entry['room']]:
|
||||||
|
got_online = True
|
||||||
|
self.rooms[entry['room']][entry['nick']] = entry
|
||||||
|
log.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
|
||||||
|
self.xmpp.event("groupchat_presence", pr)
|
||||||
|
self.xmpp.event("muc::%s::presence" % entry['room'], pr)
|
||||||
|
if got_offline:
|
||||||
|
self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
|
||||||
|
if got_online:
|
||||||
|
self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
|
||||||
|
|
||||||
def handle_groupchat_subject(self, msg):
|
def handle_groupchat_message(self, msg):
|
||||||
""" Handle a message coming from a muc indicating
|
""" Handle a message event in a muc.
|
||||||
a change of subject (or announcing it when joining the room)
|
"""
|
||||||
"""
|
self.xmpp.event('groupchat_message', msg)
|
||||||
self.xmpp.event('groupchat_subject', msg)
|
self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
|
||||||
|
|
||||||
def jidInRoom(self, room, jid):
|
def handle_groupchat_subject(self, msg):
|
||||||
for nick in self.rooms[room]:
|
""" Handle a message coming from a muc indicating
|
||||||
entry = self.rooms[room][nick]
|
a change of subject (or announcing it when joining the room)
|
||||||
if entry is not None and entry['jid'].full == jid:
|
"""
|
||||||
return True
|
self.xmpp.event('groupchat_subject', msg)
|
||||||
return False
|
|
||||||
|
|
||||||
def getNick(self, room, jid):
|
def jidInRoom(self, room, jid):
|
||||||
for nick in self.rooms[room]:
|
for nick in self.rooms[room]:
|
||||||
entry = self.rooms[room][nick]
|
entry = self.rooms[room][nick]
|
||||||
if entry is not None and entry['jid'].full == jid:
|
if entry is not None and entry['jid'].full == jid:
|
||||||
return nick
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def getRoomForm(self, room, ifrom=None):
|
def getNick(self, room, jid):
|
||||||
iq = self.xmpp.makeIqGet()
|
for nick in self.rooms[room]:
|
||||||
iq['to'] = room
|
entry = self.rooms[room][nick]
|
||||||
if ifrom is not None:
|
if entry is not None and entry['jid'].full == jid:
|
||||||
iq['from'] = ifrom
|
return nick
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
|
||||||
iq.append(query)
|
|
||||||
result = iq.send()
|
|
||||||
if result['type'] == 'error':
|
|
||||||
return False
|
|
||||||
xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
|
||||||
if xform is None: return False
|
|
||||||
form = self.xmpp.plugin['old_0004'].buildForm(xform)
|
|
||||||
return form
|
|
||||||
|
|
||||||
def configureRoom(self, room, form=None, ifrom=None):
|
def getRoomForm(self, room, ifrom=None):
|
||||||
if form is None:
|
iq = self.xmpp.makeIqGet()
|
||||||
form = self.getRoomForm(room, ifrom=ifrom)
|
iq['to'] = room
|
||||||
#form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
|
if ifrom is not None:
|
||||||
#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
|
iq['from'] = ifrom
|
||||||
iq = self.xmpp.makeIqSet()
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
iq['to'] = room
|
iq.append(query)
|
||||||
if ifrom is not None:
|
result = iq.send()
|
||||||
iq['from'] = ifrom
|
if result['type'] == 'error':
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
return False
|
||||||
form = form.getXML('submit')
|
xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
||||||
query.append(form)
|
if xform is None: return False
|
||||||
iq.append(query)
|
form = self.xmpp.plugin['old_0004'].buildForm(xform)
|
||||||
result = iq.send()
|
return form
|
||||||
if result['type'] == 'error':
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
|
def configureRoom(self, room, form=None, ifrom=None):
|
||||||
""" Join the specified room, requesting 'maxhistory' lines of history.
|
if form is None:
|
||||||
"""
|
form = self.getRoomForm(room, ifrom=ifrom)
|
||||||
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
|
#form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
|
||||||
x = ET.Element('{http://jabber.org/protocol/muc}x')
|
#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
|
||||||
if password:
|
iq = self.xmpp.makeIqSet()
|
||||||
passelement = ET.Element('password')
|
iq['to'] = room
|
||||||
passelement.text = password
|
if ifrom is not None:
|
||||||
x.append(passelement)
|
iq['from'] = ifrom
|
||||||
if maxhistory:
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
history = ET.Element('history')
|
form = form.getXML('submit')
|
||||||
if maxhistory == "0":
|
query.append(form)
|
||||||
history.attrib['maxchars'] = maxhistory
|
iq.append(query)
|
||||||
else:
|
result = iq.send()
|
||||||
history.attrib['maxstanzas'] = maxhistory
|
if result['type'] == 'error':
|
||||||
x.append(history)
|
return False
|
||||||
stanza.append(x)
|
return True
|
||||||
if not wait:
|
|
||||||
self.xmpp.send(stanza)
|
|
||||||
else:
|
|
||||||
#wait for our own room presence back
|
|
||||||
expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
|
|
||||||
self.xmpp.send(stanza, expect)
|
|
||||||
self.rooms[room] = {}
|
|
||||||
self.ourNicks[room] = nick
|
|
||||||
|
|
||||||
def destroy(self, room, reason='', altroom = '', ifrom=None):
|
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
|
||||||
iq = self.xmpp.makeIqSet()
|
""" Join the specified room, requesting 'maxhistory' lines of history.
|
||||||
if ifrom is not None:
|
"""
|
||||||
iq['from'] = ifrom
|
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
|
||||||
iq['to'] = room
|
x = ET.Element('{http://jabber.org/protocol/muc}x')
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
if password:
|
||||||
destroy = ET.Element('destroy')
|
passelement = ET.Element('password')
|
||||||
if altroom:
|
passelement.text = password
|
||||||
destroy.attrib['jid'] = altroom
|
x.append(passelement)
|
||||||
xreason = ET.Element('reason')
|
if maxhistory:
|
||||||
xreason.text = reason
|
history = ET.Element('history')
|
||||||
destroy.append(xreason)
|
if maxhistory == "0":
|
||||||
query.append(destroy)
|
history.attrib['maxchars'] = maxhistory
|
||||||
iq.append(query)
|
else:
|
||||||
r = iq.send()
|
history.attrib['maxstanzas'] = maxhistory
|
||||||
if r is False or r['type'] == 'error':
|
x.append(history)
|
||||||
return False
|
stanza.append(x)
|
||||||
return True
|
if not wait:
|
||||||
|
self.xmpp.send(stanza)
|
||||||
|
else:
|
||||||
|
#wait for our own room presence back
|
||||||
|
expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
|
||||||
|
self.xmpp.send(stanza, expect)
|
||||||
|
self.rooms[room] = {}
|
||||||
|
self.ourNicks[room] = nick
|
||||||
|
|
||||||
def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
|
def destroy(self, room, reason='', altroom = '', ifrom=None):
|
||||||
""" Change room affiliation."""
|
iq = self.xmpp.makeIqSet()
|
||||||
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
|
if ifrom is not None:
|
||||||
raise TypeError
|
iq['from'] = ifrom
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
|
iq['to'] = room
|
||||||
if nick is not None:
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
|
destroy = ET.Element('destroy')
|
||||||
else:
|
if altroom:
|
||||||
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
|
destroy.attrib['jid'] = altroom
|
||||||
query.append(item)
|
xreason = ET.Element('reason')
|
||||||
iq = self.xmpp.makeIqSet(query)
|
xreason.text = reason
|
||||||
iq['to'] = room
|
destroy.append(xreason)
|
||||||
result = iq.send()
|
query.append(destroy)
|
||||||
if result is False or result['type'] != 'result':
|
iq.append(query)
|
||||||
raise ValueError
|
r = iq.send()
|
||||||
return True
|
if r is False or r['type'] == 'error':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def invite(self, room, jid, reason=''):
|
def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
|
||||||
""" Invite a jid to a room."""
|
""" Change room affiliation."""
|
||||||
msg = self.xmpp.makeMessage(room)
|
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
|
||||||
msg['from'] = self.xmpp.jid
|
raise TypeError
|
||||||
x = ET.Element('{http://jabber.org/protocol/muc#user}x')
|
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
|
||||||
invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
|
if nick is not None:
|
||||||
if reason:
|
item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
|
||||||
rxml = ET.Element('reason')
|
else:
|
||||||
rxml.text = reason
|
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
|
||||||
invite.append(rxml)
|
query.append(item)
|
||||||
x.append(invite)
|
iq = self.xmpp.makeIqSet(query)
|
||||||
msg.append(x)
|
iq['to'] = room
|
||||||
self.xmpp.send(msg)
|
result = iq.send()
|
||||||
|
if result is False or result['type'] != 'result':
|
||||||
|
raise ValueError
|
||||||
|
return True
|
||||||
|
|
||||||
def leaveMUC(self, room, nick, msg=''):
|
def invite(self, room, jid, reason='', mfrom=''):
|
||||||
""" Leave the specified room.
|
""" Invite a jid to a room."""
|
||||||
"""
|
msg = self.xmpp.makeMessage(room)
|
||||||
if msg:
|
msg['from'] = mfrom
|
||||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
|
x = ET.Element('{http://jabber.org/protocol/muc#user}x')
|
||||||
else:
|
invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
|
||||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
|
if reason:
|
||||||
del self.rooms[room]
|
rxml = ET.Element('reason')
|
||||||
|
rxml.text = reason
|
||||||
|
invite.append(rxml)
|
||||||
|
x.append(invite)
|
||||||
|
msg.append(x)
|
||||||
|
self.xmpp.send(msg)
|
||||||
|
|
||||||
def getRoomConfig(self, room):
|
def leaveMUC(self, room, nick, msg=''):
|
||||||
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
|
""" Leave the specified room.
|
||||||
iq['to'] = room
|
"""
|
||||||
iq['from'] = self.xmpp.jid
|
if msg:
|
||||||
result = iq.send()
|
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
|
||||||
if result is None or result['type'] != 'result':
|
else:
|
||||||
raise ValueError
|
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
|
||||||
form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
del self.rooms[room]
|
||||||
if form is None:
|
|
||||||
raise ValueError
|
|
||||||
return self.xmpp.plugin['xep_0004'].buildForm(form)
|
|
||||||
|
|
||||||
def cancelConfig(self, room):
|
def getRoomConfig(self, room, ifrom=''):
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
|
||||||
x = ET.Element('{jabber:x:data}x', type='cancel')
|
iq['to'] = room
|
||||||
query.append(x)
|
iq['from'] = ifrom
|
||||||
iq = self.xmpp.makeIqSet(query)
|
result = iq.send()
|
||||||
iq.send()
|
if result is None or result['type'] != 'result':
|
||||||
|
raise ValueError
|
||||||
|
form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
||||||
|
if form is None:
|
||||||
|
raise ValueError
|
||||||
|
return self.xmpp.plugin['xep_0004'].buildForm(form)
|
||||||
|
|
||||||
def setRoomConfig(self, room, config):
|
def cancelConfig(self, room):
|
||||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
x = config.getXML('submit')
|
x = ET.Element('{jabber:x:data}x', type='cancel')
|
||||||
query.append(x)
|
query.append(x)
|
||||||
iq = self.xmpp.makeIqSet(query)
|
iq = self.xmpp.makeIqSet(query)
|
||||||
iq['to'] = room
|
iq.send()
|
||||||
iq['from'] = self.xmpp.jid
|
|
||||||
iq.send()
|
|
||||||
|
|
||||||
def getJoinedRooms(self):
|
def setRoomConfig(self, room, config, ifrom=''):
|
||||||
return self.rooms.keys()
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
|
x = config.getXML('submit')
|
||||||
|
query.append(x)
|
||||||
|
iq = self.xmpp.makeIqSet(query)
|
||||||
|
iq['to'] = room
|
||||||
|
iq['from'] = ifrom
|
||||||
|
iq.send()
|
||||||
|
|
||||||
def getOurJidInRoom(self, roomJid):
|
def getJoinedRooms(self):
|
||||||
""" Return the jid we're using in a room.
|
return self.rooms.keys()
|
||||||
"""
|
|
||||||
return "%s/%s" % (roomJid, self.ourNicks[roomJid])
|
|
||||||
|
|
||||||
def getJidProperty(self, room, nick, jidProperty):
|
def getOurJidInRoom(self, roomJid):
|
||||||
""" Get the property of a nick in a room, such as its 'jid' or 'affiliation'
|
""" Return the jid we're using in a room.
|
||||||
If not found, return None.
|
"""
|
||||||
"""
|
return "%s/%s" % (roomJid, self.ourNicks[roomJid])
|
||||||
if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
|
|
||||||
return self.rooms[room][nick][jidProperty]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getRoster(self, room):
|
def getJidProperty(self, room, nick, jidProperty):
|
||||||
""" Get the list of nicks in a room.
|
""" Get the property of a nick in a room, such as its 'jid' or 'affiliation'
|
||||||
"""
|
If not found, return None.
|
||||||
if room not in self.rooms.keys():
|
"""
|
||||||
return None
|
if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
|
||||||
return self.rooms[room].keys()
|
return self.rooms[room][nick][jidProperty]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getRoster(self, room):
|
||||||
|
""" Get the list of nicks in a room.
|
||||||
|
"""
|
||||||
|
if room not in self.rooms.keys():
|
||||||
|
return None
|
||||||
|
return self.rooms[room].keys()
|
||||||
|
|
|
@ -110,7 +110,7 @@ class xep_0050(base.base_plugin):
|
||||||
if not id:
|
if not id:
|
||||||
id = self.xmpp.getNewId()
|
id = self.xmpp.getNewId()
|
||||||
iq = self.xmpp.makeIqResult(id)
|
iq = self.xmpp.makeIqResult(id)
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
iq.attrib['to'] = to
|
iq.attrib['to'] = to
|
||||||
command = ET.Element('{http://jabber.org/protocol/commands}command')
|
command = ET.Element('{http://jabber.org/protocol/commands}command')
|
||||||
command.attrib['node'] = node
|
command.attrib['node'] = node
|
||||||
|
|
|
@ -51,7 +51,7 @@ class xep_0060(base.base_plugin):
|
||||||
pubsub.append(configure)
|
pubsub.append(configure)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is False or result is None or result['type'] == 'error': return False
|
if result is False or result is None or result['type'] == 'error': return False
|
||||||
|
@ -63,15 +63,15 @@ class xep_0060(base.base_plugin):
|
||||||
subscribe.attrib['node'] = node
|
subscribe.attrib['node'] = node
|
||||||
if subscribee is None:
|
if subscribee is None:
|
||||||
if bare:
|
if bare:
|
||||||
subscribe.attrib['jid'] = self.xmpp.jid
|
subscribe.attrib['jid'] = self.xmpp.boundjid.bare
|
||||||
else:
|
else:
|
||||||
subscribe.attrib['jid'] = self.xmpp.fulljid
|
subscribe.attrib['jid'] = self.xmpp.boundjid.full
|
||||||
else:
|
else:
|
||||||
subscribe.attrib['jid'] = subscribee
|
subscribe.attrib['jid'] = subscribee
|
||||||
pubsub.append(subscribe)
|
pubsub.append(subscribe)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is False or result is None or result['type'] == 'error': return False
|
if result is False or result is None or result['type'] == 'error': return False
|
||||||
|
@ -83,15 +83,15 @@ class xep_0060(base.base_plugin):
|
||||||
unsubscribe.attrib['node'] = node
|
unsubscribe.attrib['node'] = node
|
||||||
if subscribee is None:
|
if subscribee is None:
|
||||||
if bare:
|
if bare:
|
||||||
unsubscribe.attrib['jid'] = self.xmpp.jid
|
unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare
|
||||||
else:
|
else:
|
||||||
unsubscribe.attrib['jid'] = self.xmpp.fulljid
|
unsubscribe.attrib['jid'] = self.xmpp.boundjid.full
|
||||||
else:
|
else:
|
||||||
unsubscribe.attrib['jid'] = subscribee
|
unsubscribe.attrib['jid'] = subscribee
|
||||||
pubsub.append(unsubscribe)
|
pubsub.append(unsubscribe)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is False or result is None or result['type'] == 'error': return False
|
if result is False or result is None or result['type'] == 'error': return False
|
||||||
|
@ -109,7 +109,7 @@ class xep_0060(base.base_plugin):
|
||||||
iq = self.xmpp.makeIqGet()
|
iq = self.xmpp.makeIqGet()
|
||||||
iq.append(pubsub)
|
iq.append(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
|
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
|
@ -133,7 +133,7 @@ class xep_0060(base.base_plugin):
|
||||||
iq = self.xmpp.makeIqGet()
|
iq = self.xmpp.makeIqGet()
|
||||||
iq.append(pubsub)
|
iq.append(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result == False or result['type'] == 'error':
|
if result is None or result == False or result['type'] == 'error':
|
||||||
|
@ -156,7 +156,7 @@ class xep_0060(base.base_plugin):
|
||||||
iq = self.xmpp.makeIqGet()
|
iq = self.xmpp.makeIqGet()
|
||||||
iq.append(pubsub)
|
iq.append(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result == False or result['type'] == 'error':
|
if result is None or result == False or result['type'] == 'error':
|
||||||
|
@ -179,7 +179,7 @@ class xep_0060(base.base_plugin):
|
||||||
pubsub.append(delete)
|
pubsub.append(delete)
|
||||||
iq.append(pubsub)
|
iq.append(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is not None and result is not False and result['type'] != 'error':
|
if result is not None and result is not False and result['type'] != 'error':
|
||||||
return True
|
return True
|
||||||
|
@ -196,7 +196,7 @@ class xep_0060(base.base_plugin):
|
||||||
pubsub.append(configure)
|
pubsub.append(configure)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result['type'] == 'error':
|
if result is None or result['type'] == 'error':
|
||||||
|
@ -217,7 +217,7 @@ class xep_0060(base.base_plugin):
|
||||||
pubsub.append(publish)
|
pubsub.append(publish)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result is False or result['type'] == 'error': return False
|
if result is None or result is False or result['type'] == 'error': return False
|
||||||
|
@ -236,7 +236,7 @@ class xep_0060(base.base_plugin):
|
||||||
pubsub.append(retract)
|
pubsub.append(retract)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result is False or result['type'] == 'error': return False
|
if result is None or result is False or result['type'] == 'error': return False
|
||||||
|
@ -287,7 +287,7 @@ class xep_0060(base.base_plugin):
|
||||||
pubsub.append(affs)
|
pubsub.append(affs)
|
||||||
iq = self.xmpp.makeIqSet(pubsub)
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
iq.attrib['to'] = ps_jid
|
iq.attrib['to'] = ps_jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq['id']
|
id = iq['id']
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result is None or result is False or result['type'] == 'error':
|
if result is None or result is False or result['type'] == 'error':
|
||||||
|
|
|
@ -42,7 +42,7 @@ class xep_0092(base.base_plugin):
|
||||||
query = ET.Element('{jabber:iq:version}query')
|
query = ET.Element('{jabber:iq:version}query')
|
||||||
iq.append(query)
|
iq.append(query)
|
||||||
iq.attrib['to'] = jid
|
iq.attrib['to'] = jid
|
||||||
iq.attrib['from'] = self.xmpp.fulljid
|
iq.attrib['from'] = self.xmpp.boundjid.full
|
||||||
id = iq.get('id')
|
id = iq.get('id')
|
||||||
result = iq.send()
|
result = iq.send()
|
||||||
if result and result is not None and result.get('type', 'error') != 'error':
|
if result and result is not None and result.get('type', 'error') != 'error':
|
||||||
|
|
|
@ -94,6 +94,8 @@ class XMLStream(object):
|
||||||
ssl_support -- Indicates if a SSL library is available for use.
|
ssl_support -- Indicates if a SSL library is available for use.
|
||||||
ssl_version -- The version of the SSL protocol to use.
|
ssl_version -- The version of the SSL protocol to use.
|
||||||
Defaults to ssl.PROTOCOL_TLSv1.
|
Defaults to ssl.PROTOCOL_TLSv1.
|
||||||
|
ca_certs -- File path to a CA certificate to verify the
|
||||||
|
server's identity.
|
||||||
state -- A state machine for managing the stream's
|
state -- A state machine for managing the stream's
|
||||||
connection state.
|
connection state.
|
||||||
stream_footer -- The start tag and any attributes for the stream's
|
stream_footer -- The start tag and any attributes for the stream's
|
||||||
|
@ -163,6 +165,7 @@ class XMLStream(object):
|
||||||
|
|
||||||
self.ssl_support = SSL_SUPPORT
|
self.ssl_support = SSL_SUPPORT
|
||||||
self.ssl_version = ssl.PROTOCOL_TLSv1
|
self.ssl_version = ssl.PROTOCOL_TLSv1
|
||||||
|
self.ca_certs = None
|
||||||
|
|
||||||
self.response_timeout = RESPONSE_TIMEOUT
|
self.response_timeout = RESPONSE_TIMEOUT
|
||||||
|
|
||||||
|
@ -283,7 +286,15 @@ class XMLStream(object):
|
||||||
self.socket.settimeout(None)
|
self.socket.settimeout(None)
|
||||||
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")
|
||||||
ssl_socket = ssl.wrap_socket(self.socket)
|
if self.ca_certs is None:
|
||||||
|
cert_policy = ssl.CERT_NONE
|
||||||
|
else:
|
||||||
|
cert_policy = ssl.CERT_REQUIRED
|
||||||
|
|
||||||
|
ssl_socket = ssl.wrap_socket(self.socket,
|
||||||
|
ca_certs=self.ca_certs,
|
||||||
|
certs_reqs=cert_policy)
|
||||||
|
|
||||||
if hasattr(self.socket, 'socket'):
|
if hasattr(self.socket, 'socket'):
|
||||||
# We are using a testing socket, so preserve the top
|
# We are using a testing socket, so preserve the top
|
||||||
# layer of wrapping.
|
# layer of wrapping.
|
||||||
|
@ -387,9 +398,17 @@ class XMLStream(object):
|
||||||
if self.ssl_support:
|
if self.ssl_support:
|
||||||
log.info("Negotiating TLS")
|
log.info("Negotiating TLS")
|
||||||
log.info("Using SSL version: %s" % str(self.ssl_version))
|
log.info("Using SSL version: %s" % str(self.ssl_version))
|
||||||
|
if self.ca_certs is None:
|
||||||
|
cert_policy = ssl.CERT_NONE
|
||||||
|
else:
|
||||||
|
cert_policy = ssl.CERT_REQUIRED
|
||||||
|
|
||||||
ssl_socket = ssl.wrap_socket(self.socket,
|
ssl_socket = ssl.wrap_socket(self.socket,
|
||||||
ssl_version=self.ssl_version,
|
ssl_version=self.ssl_version,
|
||||||
do_handshake_on_connect=False)
|
do_handshake_on_connect=False,
|
||||||
|
ca_certs=self.ca_certs,
|
||||||
|
cert_reqs=cert_policy)
|
||||||
|
|
||||||
if hasattr(self.socket, 'socket'):
|
if hasattr(self.socket, 'socket'):
|
||||||
# We are using a testing socket, so preserve the top
|
# We are using a testing socket, so preserve the top
|
||||||
# layer of wrapping.
|
# layer of wrapping.
|
||||||
|
|
Loading…
Reference in a new issue