diff options
-rw-r--r-- | setup.py | 1 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/disco.py | 2 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0085.py | 104 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0085/__init__.py | 10 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0085/chat_states.py | 49 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0085/stanza.py | 73 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0092/version.py | 2 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0199/ping.py | 6 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/xmlstream.py | 54 | ||||
-rw-r--r-- | tests/test_stanza_xep_0085.py | 37 | ||||
-rw-r--r-- | tests/test_stream_xep_0085.py | 59 |
11 files changed, 266 insertions, 131 deletions
@@ -50,6 +50,7 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0030',
'sleekxmpp/plugins/xep_0030/stanza',
'sleekxmpp/plugins/xep_0059',
+ 'sleekxmpp/plugins/xep_0085',
'sleekxmpp/plugins/xep_0092',
'sleekxmpp/plugins/xep_0199',
]
diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 45d6931b..1c967bd5 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -119,7 +119,7 @@ class xep_0030(base_plugin): def post_init(self): """Handle cross-plugin dependencies.""" base_plugin.post_init(self) - if self.xmpp['xep_0059']: + if 'xep_0059' in self.xmpp.plugin: register_stanza_plugin(DiscoItems, self.xmpp['xep_0059'].stanza.Set) diff --git a/sleekxmpp/plugins/xep_0085.py b/sleekxmpp/plugins/xep_0085.py deleted file mode 100644 index 3627e718..00000000 --- a/sleekxmpp/plugins/xep_0085.py +++ /dev/null @@ -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) diff --git a/sleekxmpp/plugins/xep_0085/__init__.py b/sleekxmpp/plugins/xep_0085/__init__.py new file mode 100644 index 00000000..ff882f05 --- /dev/null +++ b/sleekxmpp/plugins/xep_0085/__init__.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0085/chat_states.py b/sleekxmpp/plugins/xep_0085/chat_states.py new file mode 100644 index 00000000..4fb21ba0 --- /dev/null +++ b/sleekxmpp/plugins/xep_0085/chat_states.py @@ -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) diff --git a/sleekxmpp/plugins/xep_0085/stanza.py b/sleekxmpp/plugins/xep_0085/stanza.py new file mode 100644 index 00000000..8c46758c --- /dev/null +++ b/sleekxmpp/plugins/xep_0085/stanza.py @@ -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) diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index fb3671e4..46bb27f7 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -42,7 +42,7 @@ class xep_0092(base_plugin): self.xmpp.register_handler( Callback('Software Version', - StanzaPath('iq/software_version'), + StanzaPath('iq@=get/software_version'), self._handle_version)) register_stanza_plugin(Iq, Version) diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py index 064af4ca..d1e08e61 100644 --- a/sleekxmpp/plugins/xep_0199/ping.py +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -54,7 +54,7 @@ class xep_0199(base_plugin): self.xep = '0199' 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.timeout = self.config.get('timeout', 30) @@ -90,7 +90,7 @@ class xep_0199(base_plugin): """Send ping request to the server.""" log.debug("Pinging...") 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." + \ "Requesting Reconnect.") self.xmpp.reconnect() @@ -160,4 +160,4 @@ class xep_0199(base_plugin): # Backwards compatibility for names -Ping.sendPing = Ping.send_ping +xep_0199.sendPing = xep_0199.send_ping diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index a5151d7b..1c165562 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -10,6 +10,7 @@ from __future__ import with_statement, unicode_literals import copy import logging +import signal import socket as Socket import ssl import sys @@ -195,6 +196,53 @@ class XMLStream(object): self.auto_reconnect = True 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): """ Generate and return a new stream ID in hexadecimal form. @@ -305,8 +353,7 @@ class XMLStream(object): self.send_raw(self.stream_footer) # Wait for confirmation that the stream was # closed in the other direction. - if not reconnect: - self.auto_reconnect = False + self.auto_reconnect = reconnect self.stream_end_event.wait(4) if not self.auto_reconnect: self.stop.set() @@ -731,6 +778,7 @@ class XMLStream(object): if not self.stop.isSet() and self.auto_reconnect: self.reconnect() else: + self.event('killed', direct=True) self.disconnect() self.event_queue.put(('quit', None, None)) self.scheduler.run = False @@ -909,6 +957,7 @@ class XMLStream(object): return False except KeyboardInterrupt: log.debug("Keyboard Escape Detected in _event_runner") + self.event('killed', direct=True) self.disconnect() return except SystemExit: @@ -934,6 +983,7 @@ class XMLStream(object): self.disconnect(self.auto_reconnect) except KeyboardInterrupt: log.debug("Keyboard Escape Detected in _send_thread") + self.event('killed', direct=True) self.disconnect() return except SystemExit: diff --git a/tests/test_stanza_xep_0085.py b/tests/test_stanza_xep_0085.py index 5db7139a..b08404e2 100644 --- a/tests/test_stanza_xep_0085.py +++ b/tests/test_stanza_xep_0085.py @@ -4,11 +4,7 @@ import sleekxmpp.plugins.xep_0085 as xep_0085 class TestChatStates(SleekTest): def setUp(self): - register_stanza_plugin(Message, xep_0085.Active) - 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) + register_stanza_plugin(Message, xep_0085.ChatState) def testCreateChatState(self): """Testing creating chat states.""" @@ -20,25 +16,26 @@ class TestChatStates(SleekTest): """ msg = self.Message() - msg['chat_state'].active() - self.check(msg, xmlstring % 'active', - use_values=False) - msg['chat_state'].composing() - self.check(msg, xmlstring % 'composing', - use_values=False) + self.assertEqual(msg['chat_state'], '') + self.check(msg, "<message />", use_values=False) + msg['chat_state'] = 'active' + self.check(msg, xmlstring % 'active', use_values=False) - msg['chat_state'].gone() - self.check(msg, xmlstring % 'gone', - use_values=False) + msg['chat_state'] = 'composing' + self.check(msg, xmlstring % 'composing', use_values=False) - msg['chat_state'].inactive() - self.check(msg, xmlstring % 'inactive', - use_values=False) + msg['chat_state'] = 'gone' + self.check(msg, xmlstring % 'gone', use_values=False) - msg['chat_state'].paused() - self.check(msg, xmlstring % 'paused', - use_values=False) + msg['chat_state'] = 'inactive' + self.check(msg, xmlstring % 'inactive', 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) diff --git a/tests/test_stream_xep_0085.py b/tests/test_stream_xep_0085.py new file mode 100644 index 00000000..2a814805 --- /dev/null +++ b/tests/test_stream_xep_0085.py @@ -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) |