Merge branch 'develop' into stream_features

This commit is contained in:
Lance Stout 2011-03-18 15:51:44 -04:00
commit f2c99798a6
11 changed files with 266 additions and 131 deletions

View file

@ -50,6 +50,7 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0030', 'sleekxmpp/plugins/xep_0030',
'sleekxmpp/plugins/xep_0030/stanza', 'sleekxmpp/plugins/xep_0030/stanza',
'sleekxmpp/plugins/xep_0059', 'sleekxmpp/plugins/xep_0059',
'sleekxmpp/plugins/xep_0085',
'sleekxmpp/plugins/xep_0092', 'sleekxmpp/plugins/xep_0092',
'sleekxmpp/plugins/xep_0199', 'sleekxmpp/plugins/xep_0199',
] ]

View file

@ -119,7 +119,7 @@ class xep_0030(base_plugin):
def post_init(self): def post_init(self):
"""Handle cross-plugin dependencies.""" """Handle cross-plugin dependencies."""
base_plugin.post_init(self) base_plugin.post_init(self)
if self.xmpp['xep_0059']: if 'xep_0059' in self.xmpp.plugin:
register_stanza_plugin(DiscoItems, register_stanza_plugin(DiscoItems,
self.xmpp['xep_0059'].stanza.Set) self.xmpp['xep_0059'].stanza.Set)

View file

@ -1,104 +0,0 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permissio
"""
import logging
from . import base
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.message import Message
log = logging.getLogger(__name__)
class ChatState(ElementBase):
namespace = 'http://jabber.org/protocol/chatstates'
plugin_attrib = 'chat_state'
interface = set(('state',))
states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
def active(self):
self.setState('active')
def composing(self):
self.setState('composing')
def gone(self):
self.setState('gone')
def inactive(self):
self.setState('inactive')
def paused(self):
self.setState('paused')
def setState(self, state):
if state in self.states:
self.name = state
self.xml.tag = '{%s}%s' % (self.namespace, state)
else:
raise ValueError('Invalid chat state')
def getState(self):
return self.name
# In order to match the various chat state elements,
# we need one stanza object per state, even though
# they are all the same except for the initial name
# value. Do not depend on the type of the chat state
# stanza object for the actual state.
class Active(ChatState):
name = 'active'
class Composing(ChatState):
name = 'composing'
class Gone(ChatState):
name = 'gone'
class Inactive(ChatState):
name = 'inactive'
class Paused(ChatState):
name = 'paused'
class xep_0085(base.base_plugin):
"""
XEP-0085 Chat State Notifications
"""
def plugin_init(self):
self.xep = '0085'
self.description = 'Chat State Notifications'
handlers = [('Active Chat State', 'active'),
('Composing Chat State', 'composing'),
('Gone Chat State', 'gone'),
('Inactive Chat State', 'inactive'),
('Paused Chat State', 'paused')]
for handler in handlers:
self.xmpp.registerHandler(
Callback(handler[0],
MatchXPath("{%s}message/{%s}%s" % (self.xmpp.default_ns,
ChatState.namespace,
handler[1])),
self._handleChatState))
registerStanzaPlugin(Message, Active)
registerStanzaPlugin(Message, Composing)
registerStanzaPlugin(Message, Gone)
registerStanzaPlugin(Message, Inactive)
registerStanzaPlugin(Message, Paused)
def post_init(self):
base.base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/chatstates')
def _handleChatState(self, msg):
state = msg['chat_state'].name
log.debug("Chat State: %s, %s" % (state, msg['from'].jid))
self.xmpp.event('chatstate_%s' % state, msg)

View file

@ -0,0 +1,10 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permissio
"""
from sleekxmpp.plugins.xep_0085.stanza import ChatState
from sleekxmpp.plugins.xep_0085.chat_states import xep_0085

View file

@ -0,0 +1,49 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permissio
"""
import logging
import sleekxmpp
from sleekxmpp.stanza import Message
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins.xep_0085 import stanza, ChatState
log = logging.getLogger(__name__)
class xep_0085(base_plugin):
"""
XEP-0085 Chat State Notifications
"""
def plugin_init(self):
self.xep = '0085'
self.description = 'Chat State Notifications'
self.stanza = stanza
for state in ChatState.states:
self.xmpp.register_handler(
Callback('Chat State: %s' % state,
StanzaPath('message@chat_state=%s' % state),
self._handle_chat_state))
register_stanza_plugin(Message, ChatState)
def post_init(self):
base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace)
def _handle_chat_state(self, msg):
state = msg['chat_state']
log.debug("Chat State: %s, %s" % (state, msg['from'].jid))
self.xmpp.event('chatstate_%s' % state, msg)

View file

@ -0,0 +1,73 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permissio
"""
import sleekxmpp
from sleekxmpp.xmlstream import ElementBase, ET
class ChatState(ElementBase):
"""
Example chat state stanzas:
<message>
<active xmlns="http://jabber.org/protocol/chatstates" />
</message>
<message>
<paused xmlns="http://jabber.org/protocol/chatstates" />
</message>
Stanza Interfaces:
chat_state
Attributes:
states
Methods:
get_chat_state
set_chat_state
del_chat_state
"""
name = ''
namespace = 'http://jabber.org/protocol/chatstates'
plugin_attrib = 'chat_state'
interfaces = set(('chat_state',))
is_extension = True
states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
def setup(self, xml=None):
self.xml = ET.Element('')
return True
def get_chat_state(self):
parent = self.parent()
for state in self.states:
state_xml = parent.find('{%s}%s' % (self.namespace, state))
if state_xml is not None:
self.xml = state_xml
return state
return ''
def set_chat_state(self, state):
self.del_chat_state()
parent = self.parent()
if state in self.states:
self.xml = ET.Element('{%s}%s' % (self.namespace, state))
parent.append(self.xml)
elif state not in [None, '']:
raise ValueError('Invalid chat state')
def del_chat_state(self):
parent = self.parent()
for state in self.states:
state_xml = parent.find('{%s}%s' % (self.namespace, state))
if state_xml is not None:
self.xml = ET.Element('')
parent.xml.remove(state_xml)

View file

@ -42,7 +42,7 @@ class xep_0092(base_plugin):
self.xmpp.register_handler( self.xmpp.register_handler(
Callback('Software Version', Callback('Software Version',
StanzaPath('iq/software_version'), StanzaPath('iq@=get/software_version'),
self._handle_version)) self._handle_version))
register_stanza_plugin(Iq, Version) register_stanza_plugin(Iq, Version)

View file

@ -54,7 +54,7 @@ class xep_0199(base_plugin):
self.xep = '0199' self.xep = '0199'
self.stanza = stanza self.stanza = stanza
self.keepalive = self.config.get('keepalive', True) self.keepalive = self.config.get('keepalive', False)
self.frequency = float(self.config.get('frequency', 300)) self.frequency = float(self.config.get('frequency', 300))
self.timeout = self.config.get('timeout', 30) self.timeout = self.config.get('timeout', 30)
@ -90,7 +90,7 @@ class xep_0199(base_plugin):
"""Send ping request to the server.""" """Send ping request to the server."""
log.debug("Pinging...") log.debug("Pinging...")
resp = self.send_ping(self.xmpp.boundjid.host, self.timeout) resp = self.send_ping(self.xmpp.boundjid.host, self.timeout)
if not resp: if resp is None or resp is False:
log.debug("Did not recieve ping back in time." + \ log.debug("Did not recieve ping back in time." + \
"Requesting Reconnect.") "Requesting Reconnect.")
self.xmpp.reconnect() self.xmpp.reconnect()
@ -160,4 +160,4 @@ class xep_0199(base_plugin):
# Backwards compatibility for names # Backwards compatibility for names
Ping.sendPing = Ping.send_ping xep_0199.sendPing = xep_0199.send_ping

View file

@ -10,6 +10,7 @@ from __future__ import with_statement, unicode_literals
import copy import copy
import logging import logging
import signal
import socket as Socket import socket as Socket
import ssl import ssl
import sys import sys
@ -195,6 +196,53 @@ class XMLStream(object):
self.auto_reconnect = True self.auto_reconnect = True
self.is_client = False self.is_client = False
def use_signals(self, signals=None):
"""
Register signal handlers for SIGHUP and SIGTERM, if possible,
which will raise a "killed" event when the application is
terminated.
If a signal handler already existed, it will be executed first,
before the "killed" event is raised.
Arguments:
signals -- A list of signal names to be monitored.
Defaults to ['SIGHUP', 'SIGTERM'].
"""
if signals is None:
signals = ['SIGHUP', 'SIGTERM']
existing_handlers = {}
for sig_name in signals:
if hasattr(signal, sig_name):
sig = getattr(signal, sig_name)
handler = signal.getsignal(sig)
if handler:
existing_handlers[sig] = handler
def handle_kill(signum, frame):
"""
Capture kill event and disconnect cleanly after first
spawning the "killed" event.
"""
if signum in existing_handlers and \
existing_handlers[signum] != handle_kill:
existing_handlers[signum](signum, frame)
self.event("killed", direct=True)
self.disconnect()
try:
for sig_name in signals:
if hasattr(signal, sig_name):
sig = getattr(signal, sig_name)
signal.signal(sig, handle_kill)
self.__signals_installed = True
except:
log.debug("Can not set interrupt signal handlers. " + \
"SleekXMPP is not running from a main thread.")
def new_id(self): def new_id(self):
""" """
Generate and return a new stream ID in hexadecimal form. Generate and return a new stream ID in hexadecimal form.
@ -305,8 +353,7 @@ class XMLStream(object):
self.send_raw(self.stream_footer) self.send_raw(self.stream_footer)
# Wait for confirmation that the stream was # Wait for confirmation that the stream was
# closed in the other direction. # closed in the other direction.
if not reconnect: self.auto_reconnect = reconnect
self.auto_reconnect = False
self.stream_end_event.wait(4) self.stream_end_event.wait(4)
if not self.auto_reconnect: if not self.auto_reconnect:
self.stop.set() self.stop.set()
@ -731,6 +778,7 @@ class XMLStream(object):
if not self.stop.isSet() and self.auto_reconnect: if not self.stop.isSet() and self.auto_reconnect:
self.reconnect() self.reconnect()
else: else:
self.event('killed', direct=True)
self.disconnect() self.disconnect()
self.event_queue.put(('quit', None, None)) self.event_queue.put(('quit', None, None))
self.scheduler.run = False self.scheduler.run = False
@ -909,6 +957,7 @@ class XMLStream(object):
return False return False
except KeyboardInterrupt: except KeyboardInterrupt:
log.debug("Keyboard Escape Detected in _event_runner") log.debug("Keyboard Escape Detected in _event_runner")
self.event('killed', direct=True)
self.disconnect() self.disconnect()
return return
except SystemExit: except SystemExit:
@ -934,6 +983,7 @@ class XMLStream(object):
self.disconnect(self.auto_reconnect) self.disconnect(self.auto_reconnect)
except KeyboardInterrupt: except KeyboardInterrupt:
log.debug("Keyboard Escape Detected in _send_thread") log.debug("Keyboard Escape Detected in _send_thread")
self.event('killed', direct=True)
self.disconnect() self.disconnect()
return return
except SystemExit: except SystemExit:

View file

@ -4,11 +4,7 @@ import sleekxmpp.plugins.xep_0085 as xep_0085
class TestChatStates(SleekTest): class TestChatStates(SleekTest):
def setUp(self): def setUp(self):
register_stanza_plugin(Message, xep_0085.Active) register_stanza_plugin(Message, xep_0085.ChatState)
register_stanza_plugin(Message, xep_0085.Composing)
register_stanza_plugin(Message, xep_0085.Gone)
register_stanza_plugin(Message, xep_0085.Inactive)
register_stanza_plugin(Message, xep_0085.Paused)
def testCreateChatState(self): def testCreateChatState(self):
"""Testing creating chat states.""" """Testing creating chat states."""
@ -20,25 +16,26 @@ class TestChatStates(SleekTest):
""" """
msg = self.Message() msg = self.Message()
msg['chat_state'].active()
self.check(msg, xmlstring % 'active',
use_values=False)
msg['chat_state'].composing() self.assertEqual(msg['chat_state'], '')
self.check(msg, xmlstring % 'composing', self.check(msg, "<message />", use_values=False)
use_values=False)
msg['chat_state'] = 'active'
self.check(msg, xmlstring % 'active', use_values=False)
msg['chat_state'].gone() msg['chat_state'] = 'composing'
self.check(msg, xmlstring % 'gone', self.check(msg, xmlstring % 'composing', use_values=False)
use_values=False)
msg['chat_state'].inactive() msg['chat_state'] = 'gone'
self.check(msg, xmlstring % 'inactive', self.check(msg, xmlstring % 'gone', use_values=False)
use_values=False)
msg['chat_state'].paused() msg['chat_state'] = 'inactive'
self.check(msg, xmlstring % 'paused', self.check(msg, xmlstring % 'inactive', use_values=False)
use_values=False)
msg['chat_state'] = 'paused'
self.check(msg, xmlstring % 'paused', use_values=False)
del msg['chat_state']
self.check(msg, "<message />")
suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates) suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates)

View file

@ -0,0 +1,59 @@
import threading
import time
from sleekxmpp.test import *
class TestStreamChatStates(SleekTest):
def tearDown(self):
self.stream_close()
def testChatStates(self):
self.stream_start(mode='client', plugins=['xep_0030', 'xep_0085'])
results = []
def handle_state(msg):
results.append(msg['chat_state'])
self.xmpp.add_event_handler('chatstate_active', handle_state)
self.xmpp.add_event_handler('chatstate_inactive', handle_state)
self.xmpp.add_event_handler('chatstate_paused', handle_state)
self.xmpp.add_event_handler('chatstate_gone', handle_state)
self.xmpp.add_event_handler('chatstate_composing', handle_state)
self.recv("""
<message>
<active xmlns="http://jabber.org/protocol/chatstates" />
</message>
""")
self.recv("""
<message>
<inactive xmlns="http://jabber.org/protocol/chatstates" />
</message>
""")
self.recv("""
<message>
<paused xmlns="http://jabber.org/protocol/chatstates" />
</message>
""")
self.recv("""
<message>
<composing xmlns="http://jabber.org/protocol/chatstates" />
</message>
""")
self.recv("""
<message>
<gone xmlns="http://jabber.org/protocol/chatstates" />
</message>
""")
# Give event queue time to process
time.sleep(0.3)
expected = ['active', 'inactive', 'paused', 'composing', 'gone']
self.failUnless(results == expected,
"Chat state event not handled: %s" % results)
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamChatStates)