view src/plugins/plugin_xep_0313.py @ 1596:b7ee113183fc

jp: better profile commands: - new "profile/default" command - info doesn't show password anymore by default, need to be explicitly requested - info and modify don't need to connect anymore - modify can now set default profile. As use_profile is set, at least a profile session need to be started when it would not be mandatory technicaly (if just setting the profile as default is needed). But this option should not be used often, and it's not a big side effect, so I don't feel the need to create a new dedicated command, or to do complicated checks to avoid the session start.
author Goffi <goffi@goffi.org>
date Sat, 14 Nov 2015 19:18:10 +0100
parents 3265a2639182
children d17772b0fe22
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# SAT plugin for Message Archive Management (XEP-0313)
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Jérôme Poisson (goffi@goffi.org)
# Copyright (C) 2013, 2014, 2015 Adrien Cossa (souliane@mailoo.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program 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 Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from sat.core.constants import Const as C
from sat.core.i18n import _
from sat.core.log import getLogger
log = getLogger(__name__)

try:
    from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
    from wokkel.subprotocols import XMPPHandler
from twisted.words.xish import domish
from twisted.words.protocols.jabber import jid

from zope.interface import implements

from wokkel import disco, data_form
from wokkel.generic import parseXml
from wokkel.pubsub import NS_PUBSUB_EVENT, ItemsEvent

# TODO: change this when RSM and MAM are in wokkel
from sat.tmp.wokkel.rsm import RSMRequest
from sat.tmp.wokkel import mam


NS_MAM = 'urn:xmpp:mam:0'
NS_SF = 'urn:xmpp:forward:0'
NS_DD = 'urn:xmpp:delay'
NS_CLIENT = 'jabber:client'

PLUGIN_INFO = {
    "name": "Message Archive Management",
    "import_name": "XEP-0313",
    "type": "XEP",
    "protocols": ["XEP-0313"],
    "dependencies": ["XEP-0059", "XEP-0297", "XEP-0203"],
    "recommendations": ["XEP-0334"],
    "main": "XEP_0313",
    "handler": "yes",
    "description": _("""Implementation of Message Archive Management""")
}


class XEP_0313(object):

    def __init__(self, host):
        log.info(_("Message Archive Management plugin initialization"))
        self.host = host
        self.clients = {}  # bind profile name to SatMAMClient
        host.bridge.addMethod("MAMqueryFields", ".plugin", in_sign='ss', out_sign='s',
                              method=self._queryFields,
                              async=True,
                              doc={})
        host.bridge.addMethod("MAMqueryArchive", ".plugin", in_sign='ssa{ss}ss', out_sign='s',
                              method=self._queryArchive,
                              async=True,
                              doc={})
        host.bridge.addMethod("MAMgetPrefs", ".plugin", in_sign='ss', out_sign='s',
                              method=self._getPrefs,
                              async=True,
                              doc={})
        host.bridge.addMethod("MAMsetPrefs", ".plugin", in_sign='ssasass', out_sign='s',
                              method=self._setPrefs,
                              async=True,
                              doc={})
        host.trigger.add("MessageReceived", self.messageReceivedTrigger)

    def getHandler(self, profile):
        self.clients[profile] = SatMAMClient(self, profile)
        return self.clients[profile]

    def profileDisconnected(self, profile):
        try:
            del self.clients[profile]
        except KeyError:
            pass

    def _queryFields(self, service_s=None, profile_key=C.PROF_KEY_NONE):
        service = jid.JID(service_s) if service_s else None
        return self.queryFields(service, profile_key)

    def queryFields(self, service=None, profile_key=C.PROF_KEY_NONE):
        """Ask the server about additional supported fields.

        @param service: entity offering the MAM service (None for user archives)
        @param profile_key (unicode): %(doc_profile_key)s
        @return: the server response as a Deferred domish.Element
        """
        # http://xmpp.org/extensions/xep-0313.html#query-form
        def eb(failure):
            # typically StanzaError with condition u'service-unavailable'
            log.error(failure.getErrorMessage())
            return ''

        profile = self.host.memory.getProfileName(profile_key)
        d = self.clients[profile].queryFields(service)
        return d.addCallbacks(lambda elt: elt.toXml(), eb)

    def _queryArchive(self, service_s=None, form_xml=None, rsm_dict=None, node=None, profile_key=C.PROF_KEY_NONE):
        service = jid.JID(service_s) if service_s else None
        if form_xml:
            form = data_form.Form.fromElement(parseXml(form_xml))
            if form.formNamespace != NS_MAM:
                log.error(_(u"Expected a MAM Data Form, got instead: %s") % form.formNamespace)
                form = None
        else:
            form = None
        rsm = RSMRequest(**rsm_dict) if rsm_dict else None
        return self.queryArchive(service, form, rsm, node, profile_key)

    def queryArchive(self, service=None, form=None, rsm=None, node=None, profile_key=C.PROF_KEY_NONE):
        """Query a user, MUC or pubsub archive.

        @param service: entity offering the MAM service (None for user archives)
        @param form (Form): data form to filter the request
        @param rsm (RSMRequest): RSM request instance
        @param node (unicode): pubsub node to query, or None if inappropriate
        @param profile_key (unicode): %(doc_profile_key)s
        @return: a Deferred when the message has been sent
        """
        def eb(failure):
            # typically StanzaError with condition u'service-unavailable'
            log.error(failure.getErrorMessage())
            return ''

        profile = self.host.memory.getProfileName(profile_key)
        d = self.clients[profile].queryArchive(service, form, rsm, node)
        return d.addCallbacks(lambda elt: elt.toXml(), eb)
        # TODO: add the handler for receiving the final message

    def _getPrefs(self, service_s=None, profile_key=C.PROF_KEY_NONE):
        service = jid.JID(service_s) if service_s else None
        return self.getPrefs(service, profile_key)

    def getPrefs(self, service=None, profile_key=C.PROF_KEY_NONE):
        """Retrieve the current user preferences.

        @param service: entity offering the MAM service (None for user archives)
        @param profile_key (unicode): %(doc_profile_key)s
        @return: the server response as a Deferred domish.Element
        """
        # http://xmpp.org/extensions/xep-0313.html#prefs
        def eb(failure):
            # typically StanzaError with condition u'service-unavailable'
            log.error(failure.getErrorMessage())
            return ''

        profile = self.host.memory.getProfileName(profile_key)
        d = self.clients[profile].queryPrefs(service)
        return d.addCallbacks(lambda elt: elt.toXml(), eb)

    def _setPrefs(self, service_s=None, default='roster', always=None, never=None, profile_key=C.PROF_KEY_NONE):
        service = jid.JID(service_s) if service_s else None
        always_jid = [jid.JID(entity) for entity in always]
        never_jid = [jid.JID(entity) for entity in never]
        #TODO: why not build here a MAMPrefs object instead of passing the args separately?
        return self.setPrefs(service, default, always_jid, never_jid, profile_key)

    def setPrefs(self, service=None, default='roster', always=None, never=None, profile_key=C.PROF_KEY_NONE):
        """Set news user preferences.

        @param service: entity offering the MAM service (None for user archives)
        @param default (unicode): a value in ('always', 'never', 'roster')
        @param always (list): a list of JID instances
        @param never (list): a list of JID instances
        @param profile_key (unicode): %(doc_profile_key)s
        @return: the server response as a Deferred domish.Element
        """
        # http://xmpp.org/extensions/xep-0313.html#prefs
        def eb(failure):
            # typically StanzaError with condition u'service-unavailable'
            log.error(failure.getErrorMessage())
            return ''

        profile = self.host.memory.getProfileName(profile_key)
        d = self.clients[profile].setPrefs(service, default, always, never)
        return d.addCallbacks(lambda elt: elt.toXml(), eb)

    def messageReceivedTrigger(self, message, post_treat, profile):
        """Check if the message is a MAM result. If so, extract the original
        message, stop processing the current message and process the original
        message instead.
        """
        try:
            result = domish.generateElementsQNamed(message.elements(), "result", NS_MAM).next()
        except StopIteration:
            return True
        try:
            forwarded = domish.generateElementsQNamed(result.elements(), "forwarded", NS_SF).next()
        except StopIteration:
            log.error(_("MAM result misses its <forwarded/> mandatory element!"))
            return False
        try:
            # TODO: delay is not here for nothing, get benefice of it!
            delay = domish.generateElementsQNamed(forwarded.elements(), "delay", NS_DD).next()
            msg = domish.generateElementsQNamed(forwarded.elements(), "message", NS_CLIENT).next()
        except StopIteration:
            log.error(_("<forwarded/> element misses a mandatory child!"))
            return False
        log.debug(_("MAM found a forwarded message"))

        if msg.event and msg.event.uri == NS_PUBSUB_EVENT:
            event = ItemsEvent(jid.JID(message['from']),
                               jid.JID(message['to']),
                               msg.event.items['node'],
                               msg.event.items.elements(),
                               {})
            self.host.plugins["XEP-0060"].clients[profile].itemsReceived(event)
            return False

        client = self.host.getClient(profile)
        client.messageProt.onMessage(msg)
        return False


class SatMAMClient(mam.MAMClient):
    implements(disco.IDisco)

    def __init__(self, plugin_parent, profile):
        self.plugin_parent = plugin_parent
        self.host = plugin_parent.host
        self.profile = profile
        mam.MAMClient.__init__(self)

    def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
        return [disco.DiscoFeature(NS_MAM)]

    def getDiscoItems(self, requestor, target, nodeIdentifier=''):
        return []