# Copyright (c) 2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany).
# Copyright (c) 2001-2005 LOGILAB S.A. (Paris, FRANCE).
#
# http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""JabberProtocolHandler acts like a gateway between narval and the jabber
server (jabberd). It implements :

- class `JabberProtocolHandler`, that contains the methods called by Narval
  to de/activate the handler and send the data narval generated in the
  format expected by jabberd
  
- class `JabberService`, which are Jabber clients able to connect
  to a jabber server with a user specified in the activator element


TODO:
- roster handling (and more generally iq handling)
- auto registration
- deactivation / stop a single twisted based protocol handler

:version: $Revision:$  
:author: Logilab

:copyright:
  2001-2005 LOGILAB S.A. (Paris, FRANCE)
  
  2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany)

:contact:
  http://www.logilab.fr/ -- mailto:contact@logilab.fr
  
  http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com


:type JABBER_CLIENT_NS: str
:var JABBER_CLIENT_NS: the jabber request qualified name
"""

__revision__ = "$Id:$"
__docformat__ = "restructuredtext en"

import sys

from twisted.protocols import xmlstream
from twisted.protocols.jabber import client, jid
from twisted.xish import domish

from narval import NO_NS
from narval.protocol_handlers import TwistedProtocolHandler, \
     HandlerNotActivated, HandlerAlreadyActivated
from narval.serialutils import yn_value, yn_rev_value
from narval.element import ALElement, NSAttribute
from narval.xml_handlers import BaseXMLHandler
from narval.interfaces.base import IBaseIMessage, IIMessage, IIPresence
from narval.elements.chat import get_discussion

JABBER_CLIENT_NS = 'jabber:client'


class NotInForumException(Exception):
    """raised by functions / methods expecting to be called from within
    a group chat while we are not...
    """
    
def get_user(jabber_id, mtype):
    """return the user id extracted from the jabber id, handling group
    chats
    """
    if mtype == 'groupchat':
        return jabber_id.split('/')[-1]
    return jabber_id.split('@', 1)[0]

def get_user_host(jabber_id, mtype):
    """return the user id extracted from the jabber id, including @host,
    handling group chats
    """
    if mtype == 'groupchat':
        # jabber_id has the form  conference_room@conference.host/user_id
        user = jabber_id.split('/', 1)[1]
        host = jabber_id.split('@', 1)[1].split('/')[0]
        return '%s@%s' % (user, host.replace('conference.', ''))
    # jabber_id has the form  user_id@host/resource
    return jabber_id.split('/', 1)[0]


def get_server(jabber_id):
    """return the server part of the jabber id"""
    return jabber_id.split('/')[0].split('@', 1)[1]


def jabber_presence(type=None, to=None, from_=None):
    """create and return a jabber presence element (ie not wrapped by
    the narval element)
    """
    presence = domish.Element((JABBER_CLIENT_NS, 'presence'))
    if type is not None:
        presence['type'] = type
    if to is not None:
        presence['to'] = to
    if from_ is not None:
        presence['from'] = from_
    return presence

def outgoing_message(msgtext, mto, mtype='chat', mfrom=None):
    """create and return a narval outgoing message element"""
    msg = JabberRequestElement()
    msg.type = 'outgoing'
    jmsg = domish.Element((JABBER_CLIENT_NS, 'message'))
    jmsg['type'] = mtype
    jmsg['to'] = mto
    if mfrom is not None:
        jmsg['from'] = mfrom
    msg.request = jmsg
    msg.set_body(msgtext)
    return msg

def outgoing_presence(ptype=None, pto=None):
    """create and return a narval outgoing presence element"""
    msg = JabberPresenceElement()
    msg.type = 'outgoing'
    presence = jabber_presence(to=pto, type=ptype)#, from_=self.user)
    msg.request = presence
    return msg

def _ensure_server(jabber_id, host):
    """ensure the server is in the jid, add it if necessary"""
    if jabber_id and not '@' in jabber_id:
        jabber_id = '%s@%s' % (jabber_id, host)
    return jabber_id


class JabberElementHandler(BaseXMLHandler):
    """xml handler to read narval message / presence elements"""
    
    def __init__(self, elmt, context, locator):
        BaseXMLHandler.__init__(self, elmt, context, locator)
        self._body = None
        
    def start_element(self, (xmlns, name), attrs):
        if name in ['presence', 'message']:
            self.elmt.request = domish.Element((JABBER_CLIENT_NS, name))
            self.elmt.set_type(attrs.get( (NO_NS, 'type') ))
            self.elmt.set_to(attrs.get( (NO_NS, 'to') ))
            self.elmt.set_from(attrs.get( (NO_NS, 'from') ))
        elif name == 'body':
            self._body = domish.Element((JABBER_CLIENT_NS, 'body'))
        elif name == 'x':
            # FIXME - for the moment ignored
            pass
#         elif name == 'item':
#             # FIXME - for the moment ignored
#             self.elmt = None

    def end_element(self, (xmlns, name)):
        """SAX callback: close a xml node

        :type name: tuple
        :param name: the tag name as a tuple (uri, name)
        """
        if name == 'body':
            self.elmt.request.addChild(self._body)
            self._body = None
        return self.elmt

    def characters(self, content):
        if self._body is not None:
            self._body.addContent(content)
    

class JabberElement(ALElement):
    """base class for jabber elements, based on twisted jabber support

    this class is abstract and should not be instatiated
    """
    
    type = NSAttribute(NO_NS, '', str, str) # incoming / outgoing
    activator_eid = NSAttribute(NO_NS, None, int, str)

    __child_handler__ = JabberElementHandler

    def __init__(self, request=None):
        ALElement.__init__(self)
        self.request = request
        
    def children_as_xml(self, encoding='UTF-8'):
        """return the XML representation of the wrapped request element

        :type encoding: str
        :param encoding: the encoding to use in the returned string

        :rtype: str
        :return: XML string representing the element
        """
        return self.request.toXml()

    def get_type(self):
        """return the message's type"""
        try:
            return self.request['type']
        except KeyError:
            return 'normal'


    def is_from_groupchat(self):
        """
        :rtype: bool
        :return:
          true if the presence is coming from a group chat and not from
          a real user
        """
        return self.request['type'] == 'groupchat'
    
    def set_type(self, type):
        """return the message's type"""
        self.request['type'] = type

    def get_server(self, conf=None):
        """return the server used by the given message

        :type conf: bool
        :param conf:
          conference server handling:
          * if conf is None, the server is returned as it is in the jabber id
          * if conf is false and the server is the conference server, remove
            the conference prefix
          * if conf is true and the server isn't the conference server, append
            the conference prefix
        """
        server = get_server(self.request['from'])
        if conf:
            if not server.startswith('conference.'):
                server = 'conference.%s' % server
        elif conf is not None:
            if server.startswith('conference.'):
                server = server[11:]
        return server
 
    def get_from(self):
        """return the sender of the given message"""
        return self.request['from'].split('@', 1)[0]

    def set_from(self, from_value):
        """set the user id of the sender of the given message"""
        self.request['from'] = from_value
        
    def get_to(self):
        """return the user id of the receiver of the given message, handling group
        chats
        """
        return self.request['to'].split('@', 1)[0]

    def set_to(self, to):
        """set the user id of the receiver of the given message"""
        self.request['to'] = to

    def get_from_user(self):
        """return the user id of the sender of the message, handling group chats"""
        return get_user(self.request['from'], self.request['type'])

    def get_from_user_host(self):
        """return the user id of the sender of the message, including host,
        handling group chats
        """
        return get_user_host(self.request['from'], self.request['type'])

    def get_room(self):
        """return the forum id of the sender of the given message, handling group
        chats
        """
        if self.get_type() != 'groupchat':
            raise NotInForumException()
        if self.type == 'incoming':
            return self.request['from'].split('/', 1)[0]
        return self.request['to'].split('/', 1)[0]

    def get_to_user(self):
        """return the receiver of the given message"""
        return get_user(self.request['to'], self.request['type'])

    def get_to_user_host(self):
        """return the receiver of the given message, including host"""
        return get_user_host(self.request['to'], self.request['type'])

    def build_message(self, body, mto):
        """create a reply to an incoming request

        :rtype: `JabberRequestElement`
        :return: a new message ready to be sent
        """
        msg = outgoing_message(body, self._ensure_server(mto))
        msg.activator_eid = self.activator_eid
        return msg

    def build_reply(self, answer):
        """create a reply to an incoming request

        :rtype: `JabberRequestElement`
        :return: a new message ready to be sent
        """
        orig_jmsg = self.request
        mtype = orig_jmsg['type']
        # answer to a presence
        if mtype not in ('chat','groupchat'):
            mtype = 'chat'
        # remove resource from the "to" field
        msg = outgoing_message(answer, orig_jmsg['from'].split('/')[0],
                               mfrom=orig_jmsg['to'],
                               mtype=mtype)
        msg.activator_eid = self.activator_eid
        return msg

    def build_invitation(self, room, guest):
        """create a set of messages inviting guests to the given conference room
        """
        text = 'You are invited to %s by %s' % (room, self.get_to_user())
        invitation = outgoing_message(text, self._ensure_server(guest), mtype='normal')
        invitation.activator_eid = self.activator_eid
        x = domish.Element((JABBER_CLIENT_NS, 'x'))
        x['jid'] = room
        x['xmlns'] = 'jabber:x:conference'
        invitation.request.addChild(x)
        return invitation

    def build_presence(self, ptype=None, pto=None):
        presence = outgoing_presence(ptype=ptype, pto=self._ensure_server(pto))
        presence.activator_eid = self.activator_eid
        return presence

    def _ensure_server(self, jabber_id):
        """ensure the server is in the jid, add it if necessary"""
        return _ensure_server(jabber_id, self.get_server())


class JabberPresenceElement(JabberElement):
    """a memory element wrapping an incoming / outgoing jabber presence
    """
    
    __implements__ = (IIPresence,)
    
    __xml_element__ = (NO_NS, 'jabber-presence')
    

    def is_from_groupchat(self):
        """
        :rtype: bool
        :return:
          true if the presence is coming from a group chat and not from
          a real user
        """
        return 'conference' in self.get_server()

    def get_status(self):
        """return the presence'status"""
        # don't use str() since it will additionaly try to encode
        # the unicode string returned by twisted.xish.domish.Element.__str__
        #
        # i know i know...
        return self.request.status and self.request.status.__str__() or ''

    def set_status(self, status):
        """set the presence'status"""
        # FIXME: remove existant status if any
        status_elmt = domish.Element((JABBER_CLIENT_NS, 'status'))
        status_elmt.addContent(status)
        self.request.addChild(status_elmt)

    def get_show(self):
        """return the presence'show"""
        # don't use str() since it will additionaly try to encode
        # the unicode string returned by twisted.xish.domish.Element.__str__
        #
        # i know i know...
        return self.request.show and self.request.show.__str__() or ''

    def set_show(self, show):
        """set the presence'show"""
        # FIXME: remove existant show if any
        show_elmt = domish.Element((JABBER_CLIENT_NS, 'show'))
        show_elmt.addContent(show)
        self.request.addChild(show_elmt)

    def children_as_xml(self, encoding='UTF-8'):
        """return the XML representation of the wrapped request element

        :type encoding: str
        :param encoding: the encoding to use in the returned string

        :rtype: str
        :return: XML string representing the element
        """
        return self.request.toXml()


class JabberRequestElement(JabberElement):
    """a memory element wrapping an incoming / outgoing jabber message

    :type request: 
    :ivar request: the original request object

    :type type: str
    :ivar type: the type of the request, i.e. 'incoming' or 'outgoing'

    :type activator_eid: int
    :ivar activator_eid: the eid of the jabber service activator
    """
    __implements__ = (IIMessage,)    
    __xml_element__ = (NO_NS, 'jabber-request')

    def master_id(self, master_info):
        """return the master id for this protocol, taken from information
        available in the given MasterInformationElement
        """
        return master_info.jabberid
    
    def get_body(self):
        """return the content of the message"""
        # don't use str() since it will additionaly try to encode
        # the unicode string returned by twisted.xish.domish.Element.__str__
        #
        # i know i know...
        return self.request.body.__str__().strip() or ''

    def set_body(self, text):
        if self.request.body:
            self.request.children.remove(self.request.body)
        body = domish.Element((JABBER_CLIENT_NS, 'body'))
        body.addChild(text)
        self.request.addChild(body)
    
class JabberActivatorElement(ALElement):
    """a memory element used to activate a jabber handler

    :type recipe: str
    :ivar recipe:
      the name of the recipe (<cookbook>.<recipe>) used to handle queries

    :type user: str
    :ivar user: the jabber user's name

    :type password: str
    :ivar password: the jabber user's password

    :type resource: str
    :ivar resource: the jabber resource

    :type host: str
    :ivar host: the jabber server host name, default to localhost

    :type port: int
    :ivar port: the jabber server port, default to 5222

    :type register: bool
    :ivar register:
      indicates whether we should try to auto-register to the server if
      necessary
    """
    __xml_element__ = (NO_NS, 'jabber-activator')
    recipe = NSAttribute(NO_NS, None, str, str)
    host = NSAttribute(NO_NS, 'localhost', str, str)
    port = NSAttribute(NO_NS, 5222, int, str)
    user = NSAttribute(NO_NS, None, str, str)
    password = NSAttribute(NO_NS, None, str, str)
    resource = NSAttribute(NO_NS, 'narvabber', str, str)
    register = NSAttribute(NO_NS, False, yn_value, yn_rev_value)
    verbose = NSAttribute(NO_NS, False, yn_value, yn_rev_value)

    def master_id(self, master_info):
        """return the master id for this protocol, taken from information
        available in the given MasterInformationElement
        """
        return master_info.jabberid

    def create_msg(self, msgtext, mto, mtype='chat'):
        
        """create a raw message

        :rtype: `JabberRequestElement`
        :return: a new message ready to be sent
        """
        msg = outgoing_message(msgtext, self._ensure_server(mto), mtype=mtype)
        msg.activator_eid = self.eid
        return msg

    def create_presence(self, pto, ptype='available'):
        """create a raw presence

        :rtype: `JabberRequestElement`
        :return: a new message ready to be sent
        """
        msg = outgoing_presence(ptype, self._ensure_server(pto))
        msg.activator_eid = self.eid
        return msg

    def _ensure_server(self, jabber_id):
        """ensure the server is in the jid, add it if necessary"""
        return _ensure_server(jabber_id, self.host)


class JabberProtocolHandler(TwistedProtocolHandler):
    """a Jabber client delegating non jabber specific operations to the narval
    interpreter

    :type narval: `narval.engine.Narval`
    :ivar narval: a reference to the narval interpreter
    """
    namespaces = TwistedProtocolHandler.namespaces + [('jabber', JABBER_CLIENT_NS)]
    
    def __init__(self, narval):
        TwistedProtocolHandler.__init__(self, narval)
        # this prototype is not complete.
        # will see the spec. in Jabber doc, to inform the non-facultative
        # elements
        self.prototype = (
            ('port on which the server listens, recipe to instanciate',
             ('isinstance(elmt, JabberActivatorElement) and elmt.recipe \
and elmt.user and elmt.password',)
             ),
            ('response to a Jabber request',
             ('IBaseIMessage(elmt).type == "outgoing"',)
             #  
             # FIXME (syt) : removed "and isinstance(IBaseIMessage(elmt), JabberElement)"
             # since it doesn't work with adapters. Will have to add a test doing the same kind of verification
             # when we'll have omre than one implementation of IBaseIMessage and co
             ),
            ('Jabber request',
             ('IBaseIMessage(elmt).type == "incoming" and isinstance(IBaseIMessage(elmt), JabberElement)',)
             )
            )
        self.eid_activators = {}
        self.activations = {}
        
    def activate(self, activator):
        """activates the handler with the given element.
        
        A handler can be activated more than once, and should behave
        accordingly. For instance an HTTP handler can be activated on
        different ports. The handler should use the eid of the element
        to be able to deactivate the proper service.

        :type activator: `narval.public.ALElement`
        :param activator: the element activating the handler
        
        :raise HandlerAlreadyActivated:
          if the activate method already has been called with a semantically
          identical activator
        """
        host = activator.host
        port = activator.port
        user  = activator.user
        eid = activator.eid
        key = (user, host, port)
        if self.activations.has_key(key):
            raise HandlerAlreadyActivated(key)
        self.activations[key] = eid
        log(LOG_INFO, "Activating jabber protocol handler for %s@%s:%s", key)
        recipe = activator.recipe
        password  = activator.password
        resource  = activator.resource
        jabber_id = "%s@%s/%s" % (user, host, resource)
        myjid = jid.JID(jabber_id)
        factory = client.basicClientFactory(myjid, password)
        service = JabberService(activator, self, factory, recipe, jabber_id)
        self.eid_activators[eid] = service
        self.twisted_run(host, port, factory)
        log(LOG_INFO, "Connected to jabber server %s", host)

    def deactivate(self, eid):
        """deactivates the handler.
        
        :type eid: int
        :param eid: the eid of the element deactivating the handler
        
        :raise HandlerNotActivated:
          if the handler had not been activated with the element
        """
        try:
            service = self.eid_activators[eid]
        except KeyError:
            raise HandlerNotActivated(eid)
        # FIXME
        self.stop()
##         service = self.activations[port][0]
##         service.running = None

##     def stop(self):
##         """stops the handler : stops all threads launched by the handler, if any
##         """
##         for service, thread in self.activations.values():
##             service.running = None
##             thread.join()
        
    def handle_outgoing(self, element):
        """passes an element matching one of the inputs of the handler.
        
        :raise `HandlerNotActivated`:
          if the handler has not been activated, or has not been activated to
          handle the input, or is not expecting any input.

        :type element: `narval.public.ALElement`
        :param element: the outgoing element
        """
        # need to adapt here, the element may only be an adaptable element,
        # not a IBaseIMessage itself
        element = IBaseIMessage(element) 
        try:
            service = self.eid_activators[element.activator_eid]
            # FIXME: service may not yet have the 'send' method if not yet authenticated !
            service.send_element(element)
        except:
            log_traceback(LOG_ERR, sys.exc_info())


class JabberService:
    """a jabber service handles a connection for a given user / server

    :type jabber_id: str
    :ivar jabber_id: the jabber identifier of the user used by the handler
    
    :type protocol_handler: `JabberProtocolHandler`
    :ivar protocol_handler:
      the protocol handler which has instantiated this service

    :type recipe: str
    :ivar recipe: the name of the recipe to handle incoming messages

    :type server_addr: tuple
    :ivar server_addr: the jabber server address as a tuple (host, port)
    
    :type running: bool
    :ivar running: indicates whether the service is currently running
    """

    def __init__(self, activator, protocol_handler, factory, recipe, jabber_id):
        """Initialize the connection to a JabberServer 

        :type server_addr: tuple
        :param server_addr: the jabber server address as a tuple (host, port)

        :type protocol_handler: `JabberProtocolHandler`
        :param protocol_handler:
          the protocol handler which has instantiated this service

        :type recipe: str
        :param recipe: the name of the recipe to handle incoming messages

        :type jabber_id: str
        :param jabber_id: the jabber identifier of the user used by the handler
        """
        self.activator = activator
        self.protocol_handler = protocol_handler
        self.factory = factory
        self.recipe = recipe
        self.jabber_id = jabber_id
        self.nickname = jabber_id.split('@', 1)[0]
        self.waiting_roster = False
        self.contexts = {}
        factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.authenticate)
        factory.addBootstrap(client.BasicAuthenticator.INVALID_USER_EVENT,
                             self.invalid_user)
        factory.addBootstrap(client.BasicAuthenticator.AUTH_FAILED_EVENT,
                             self.fatal_error)
        factory.addBootstrap(client.BasicAuthenticator.REGISTER_FAILED_EVENT,
                             self.fatal_error)
    
    def process_element(self, elmt):
        """message processing method : build a narval element from the request
        and give it to the narval interpreter as context of a newly created plan

        :type elmt: `JabberRequestElement` or `JabberPresenceElement`
        :param elmt:
          the jabber request wrapped by it's corresponding narval element
        """
        elmt.type = 'incoming'
        elmt.activator_eid = self.activator.eid
        self._set_context(elmt)
        try:
            self.protocol_handler.instantiate_handler_recipe(self.recipe, elmt,
                                                             [self.activator])
        except:
            log_traceback(LOG_ERR, sys.exc_info())
            # FIXME: put a more significant xml error, of jabberPacket type!!!
            # FIXME: not sure the line below work with twisted...
            self._send("<error/>")
            raise            

    def send_element(self, elmt):
        if not hasattr(self, '_send'):
            raise Exception('not yet ready to send messages !')
        
        log(LOG_DEBUG, 'sending msg : %s', elmt.request.toXml())
        self._set_context(elmt)
        self._send(elmt.request)
        
    def _set_context(self, elmt):
        """set the thread context on the element

        :type elmt: `JabberRequestElement` or `JabberPresenceElement`
        :param elmt:
          the jabber request wrapped by it's corresponding narval element
        """
        if elmt.get_type() == 'groupchat':
            context_key = elmt.get_room()
        elif elmt.type == 'incoming':
            context_key = elmt.get_from()
        else:
            context_key = elmt.get_to()
        discussion = get_discussion(self.activator,  context_key)
        discussion.append(elmt)
        elmt.in_discussion = discussion
        elmt.discussion_mode = discussion.mode


    # callbacks methods #######################################################
    
    def authenticate(self, twxml):
        """authentication callback"""
        # bind twxml.send to self
        self._send = twxml.send
        # add observer for incoming message / presence requests
        twxml.addObserver("/iq",       self.iq_handler)
        twxml.addObserver("/presence", self.presence_handler)
        twxml.addObserver("/message",  self.message_handler)
        #twxml.addObserver('/*', self.something_handler)
        # request roster
        self.waiting_roster = True
        log(LOG_DEBUG, 'requesting roster')
        iq = domish.Element((JABBER_CLIENT_NS, 'iq'))
        iq['type'] = 'get'
        iq.addElement(('jabber:iq:roster', 'query'))
        self._send(iq)

    def invalid_user(self, twxml):
        """invalid user callback"""
        log(LOG_NOTICE, 'invalid jabber user (%s)', twxml)
        ctrl = self.activator
        # should we try to register a new account to the server ?
        if ctrl.register:
            user = ctrl.user
            log(LOG_NOTICE, 'registering user %s to the jabber server', user)
            self.factory.authenticator.registerAccount(user, ctrl.password)
        else:
            self.fatal_error(twxml)
            
    def fatal_error(self, twxml):
        """unrecoverable error callback"""
        log(LOG_ERR, 'unrecoverable error: %s', twxml.toXml())
        log(LOG_ERR, 'stopping service !')
        self.protocol_handler.deactivate(self.activator.eid)
        
    def something_handler(self, twxml):
        """handle unknown packets

        :type twxml: `twisted.xish.domish.Element`
        :param twxml: the xml stream containing the element
        """
        if self.activator.verbose:
            print '------------------------------------- got what ???'
            print twxml.toXml()

    def iq_handler(self, twxml):
        """handle a iq packet

        :type twxml: `twisted.xish.domish.Element`
        :param twxml: the xml stream containing a iq element
        """
        if self.activator.verbose:
            print '------------------------------------- got iq'
            print twxml.toXml()
        if self.waiting_roster and twxml.query.uri == 'jabber:iq:roster':
            # got roster, send presence so clients know we're actually online
            log(LOG_DEBUG, 'sending presence')
            presence = domish.Element((JABBER_CLIENT_NS, 'presence'))
            presence.addElement('status').addContent('Online')
            self._send(presence)
            self.waiting_roster = False
        
    def presence_handler(self, presence):
        """handle a presence packet

        :type presence: `twisted.xish.domish.Element`
        :param presence: the xml stream containing a presence element
        """
        if self.activator.verbose:
            print '------------------------------------- got presence'
            print presence.toXml()
        # FIXME: do this in a recipe ?
        try:
            ptype = presence['type']
        except KeyError:
            presence['type'] = ptype = 'available'
        who = presence['from'].split('/')[0]
        if ptype == 'subscribe':
            # FIXME: subscription policy
            # subscription request:
            # accept subscription and send request for subscription to
            # their presence
            log(LOG_INFO, 'subscribe request from %s', who)
            self._send(jabber_presence(to=who, type='subscribed', from_=self.jabber_id))
            self._send(jabber_presence(to=who, type='subscribe', from_=self.jabber_id))
        elif ptype == 'unsubscribe':
            # unsubscription request:
            # accept unsubscription and send request for unsubscription to
            # their presence
            log(LOG_INFO, 'unsubscribe request from %s', who)
            self._send(jabber_presence(to=who, type='unsubscribed', from_=self.jabber_id))
            self._send(jabber_presence(to=who, type='unsubscribe', from_=self.jabber_id))
        elif ptype == 'subscribed':
            log(LOG_INFO, 'we are now subscribred from %s', who)
        elif ptype == 'unsubscribed':
            log(LOG_INFO, 'we are now unsubscribred from %s', who)
        elif ptype == 'available':
            show = presence.getAttribute('show') or ''
            status = presence.getAttribute('status') or ''
            log(LOG_INFO, '%s is available (%s/%s)', (who, show, status))
        elif ptype == 'unavailable':
            log(LOG_INFO, '%s is unavailable', who)
        else:
            log(LOG_NOTICE, 'unknown presence type %r from %s', (ptype, who))
        self.process_element(JabberPresenceElement(presence))

    def message_handler(self, twxml):
        """handle a message packet

        :type twxml: `twisted.xish.domish.Element`
        :param twxml: the xml stream containing a message element
        """
        if self.activator.verbose:
            print '------------------------------------- got message'
            print twxml.toXml()
        for elmt in twxml.elements():
            # invited in a group chat ?
            # in this case, just send a presence element to accept invitation
            # and do not further processing for this message
            if elmt.uri == 'jabber:x:conference':
                nick = self.jabber_id.split('@')[0]
                presence = jabber_presence(to='%s/%s' % (elmt['jid'], nick))
                self._send(presence)
                break
            # ignore delayed / error messages
            if elmt.uri == 'jabber:x:delay' or elmt.getAttribute('type') == 'error':
                break
        else:
            from_user = get_user(twxml['from'],
                                 twxml.getAttribute('type'))
            # ignore message from myself, or from group chat (ie control
            # messages sent with the chat room as user, and in this case
            # from_user is room@server)
            if from_user != self.nickname and not '@' in from_user:
                self.process_element(JabberRequestElement(twxml))



        
def register(engine):
    """register the protocol handler and associated elements to the engine

    :type engine: `narval.engine.Narval`
    :param registry: the narval interpreter
    """
##     if engine.test_mode:
##         engine.register_protocol_handler('jabber', TestJabberProtocolHandler)
##         TestJabberActivatorElement.__name__ = 'JabberActivatorElement'
##         engine.registry.register_class(TestJabberActivatorElement)
##     else:
    engine.register_protocol_handler('jabber', JabberProtocolHandler)
    engine.registry.register_class(JabberActivatorElement)
    
    engine.registry.register_class(JabberRequestElement)
    engine.registry.register_class(JabberPresenceElement)
    # maybe usefull to use in expression testing for message or presence
    engine.registry.register_class(JabberElement)
        


## # functional test jph #########################################################

## from threading import Thread, Lock
## import time

## from narval.xml_handlers import ListHandler


## class TestJabberActivatorElement(JabberActivatorElement):
##     __xml_element__ = (NO_NS, 'jabber-activator')
    
##     __child_handler__  = ListHandler
##     list_attr = 'discussion'
##     discussion = ()

##     def children_as_xml(self, encoding='UTF-8'):
##         """return the XML representation of children in this element.

##         :type encoding: str
##         :param encoding: the encoding to use in the returned string

##         :rtype: str
##         :return: XML string representing the element
##         """
##         result = []
##         for value in self.discussion:
##             result.append('  <discussionitem>%s</discussionitem>' % (value,))
##         return '\n'.join(result)


## class TestJabberProtocolHandler(ProtocolHandler):
##     """a Jabber client delegating non jabber specific operations to the narval
##     interpreter, used to test narval's interaction with the jabber protocol
##     handler

##     :type narval: `narval.engine.Narval`
##     :ivar narval: a reference to the narval interpreter
##     """
##     namespaces = TwistedProtocolHandler.namespaces + [('jabber', JABBER_CLIENT_NS)]
    
##     def __init__(self, narval):
##         self.prototype = (
##             ('port on which the server listens, recipe to instanciate',
##              ('isinstance(elmt, JabberActivatorElement) and elmt.recipe',)
##              ),
##             ('response to a Jabber request',
##              ('IBaseIMessage(elmt).type == "outgoing"',)
##              #  
##              # FIXME (syt) : removed "and isinstance(IBaseIMessage(elmt), JabberElement)"
##              # since it doesn't work with adapters. Will have to add a test doing the same kind of verification
##              # when we'll have omre than one implementation of IBaseIMessage and co
##              ),
##             ('Jabber request',
##              ('IBaseIMessage(elmt).type == "incoming" and isinstance(IBaseIMessage(elmt), JabberElement)',)
##              )
##             )
##         self.narval = narval
        
##     def activate(self, activator):
##         """activates the handler with the given element.
        
##         A handler can be activated more than once, and should behave
##         accordingly. For instance an HTTP handler can be activated on
##         different ports. The handler should use the eid of the element
##         to be able to deactivate the proper service.

##         :type activator: `narval.public.ALElement`
##         :param activator: the element activating the handler
        
##         :raise HandlerAlreadyActivated:
##           if the activate method already has been called with a semantically
##           identical activator
##         """
##         log(LOG_INFO, "Activating test jabber protocol handler")
##         self.discussion_lock = Lock()
##         self.error = False
##         Thread(target=self.simulate_discussion, args=(activator,)).start()

##     def deactivate(self, eid):
##         pass

##     def simulate_discussion(self, activator):
##         # begin with a litle sleep to avoid starting test until initialization's end
##         time.sleep(3)
##         lock = self.discussion_lock
##         #print 'ACQUIRING'
##         lock.acquire()
##         try:
##             while activator.discussion and not self.error:
##                 # get the theorically incoming message
##                 input_sentence = activator.discussion.pop(0)
##                 # set expected answer
##                 try:
##                     self.currently_expected = activator.discussion.pop(0)
##                 except IndexError:
##                     break
##                 # create and send the incoming message as a JabberMessageElement
##                 msg = activator.create_msg(input_sentence, 'narval@toto.com')
##                 msg.set_from('user@toto.com')
##                 msg.type = 'incoming'
##                 self.instantiate_handler_recipe(activator.recipe, msg, [activator])
##                 # wait for the reply if necessary (FIXME: timeout -> use a Condition)
##                 #print 'ACQUIRING, BLOCKING'
##                 lock.acquire()
##         finally:
##             #print 'RELEASING, FINALLY'
##             lock.release()
##             self.narval.quit()
        
##     def handle_outgoing(self, element):
##         try:
##             # check answer, generate a comprehensive error if unexpected
##             # need to normalize string because the list handler may slightly change
##             # the expected message (duh?)...
##             try:
##                 assert normstr(element.get_body()) == normstr(self.currently_expected), \
##                        '%r != %r' % (normstr(element.get_body()),
##                                      normstr(self.currently_expected))
##             except:
##                 self.error = True
##                 raise
##         finally:
##             #print 'RELEASING'
##             self.discussion_lock.release()
            
## def normstr(string):
##     return ' '.join(string.split())
