view salut.py @ 3:593345584d21 default tip

replace ad-hoc commands "subscribe" and "unsubscribe" with a single "update" command
author souliane <souliane@mailoo.org>
date Wed, 02 Sep 2015 14:02:50 +0200
parents 77b77f48c975
children
line wrap: on
line source

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

# SAT plugin for account creation (experimental)
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Jérôme Poisson (goffi@goffi.org)
# Copyright (C) 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 twisted.words.xish import domish
from wokkel.subprotocols import XMPPHandler, IQHandlerMixin
from wokkel import disco, iwokkel, data_form
from twisted.words.protocols.jabber import jid
from zope.interface import implements
import uuid
import sqlite3
from os import path
from collections import OrderedDict
import gettext
gettext.install('sat', "i18n", unicode=True)

DATABASE = "salut.db"
ID_CMD_LIST = disco.DiscoIdentity("automation", "command-list")
NS_COMMANDS = "http://jabber.org/protocol/commands"
NS_SEARCH = 'jabber:iq:search'
QUERY_SEARCH = "/query[@xmlns='jabber:iq:search']"
UPDATE_CMD = "/iq[@type='set']/command[@xmlns='"+NS_COMMANDS+"' and @node='update']"
INSTRUCTIONS = _(u'Update your subscription to the Jabber search directory.')
FORM_INSTRUCTIONS = [INSTRUCTIONS]
SUBSCRIBE_DONE = _("You registration to the directory has been updated.")
UNSUBSCRIBE_DONE = _("You have been removed from the directory.")
DB_CREATE = ['PRAGMA user_version=0',
             'CREATE TABLE directory (jid TEXT PRIMARY KEY, description TEXT)']


class SalutGateway(XMPPHandler, IQHandlerMixin):
    implements(iwokkel.IDisco)

    def __init__(self, component=False):
        XMPPHandler.__init__(self)
        IQHandlerMixin.__init__(self)
        self.component = component
        self.discoHandler = disco.DiscoHandler()
        new_db = not path.exists(DATABASE)
        conn = sqlite3.connect(DATABASE)
        self.db = conn.cursor() # we use SQLite in a blocking way, performance is not such a big deal here
        if new_db:
            for statement in DB_CREATE:
                self.db.execute(statement)

    def connectionMade(self):
        print "Connected!"
        self.xmlstream.addObserver("/iq[@type='get']" + QUERY_SEARCH, self.handleFieldsRequest)
        self.xmlstream.addObserver("/iq[@type='set']" + QUERY_SEARCH, self.handleSearchRequest)
        self.xmlstream.addObserver(UPDATE_CMD, self.onUpdate)
        self.discoHandler.setHandlerParent(self.parent)

    def connectionLost(self, reason):
        print "Disconnected!"

    def handleFieldsRequest(self, request):
        """Respond to a fields request with the fields' list.
       
        @param request (domish.Element): the request to respond to
        """
        result = domish.Element((None, 'iq'))
        result['type'] = 'result'
        result['id'] = request['id']
        result['to'] = request['from']
        query_elt = result.addElement('query', NS_SEARCH)
        instructions_elt = query_elt.addElement('instructions', content=INSTRUCTIONS)
        form = data_form.Form('form', title=_('Directory search'),
                              instructions=FORM_INSTRUCTIONS,
                              formNamespace=NS_SEARCH)
        form.addField(data_form.Field('fixed', label=_('Enter part of description or jid to find somebody,')))
        form.addField(data_form.Field('fixed', label=('let empty to have a full list of people')))
        form.addField(data_form.Field('text-single', 'jid', label=_('Jabber ID')))
        form.addField(data_form.Field('text-single', 'description', label=_('Description')))
        query_elt.addChild(form.toElement())
        self.xmlstream.send(result)

    def handleSearchRequest(self, request):
        """Respond to a search request with the search results.
        
        @param request (domish.Element): the request to respond to
        """
        args = OrderedDict()
        try:
            query_elt = request.elements(NS_SEARCH, 'query').next()
            form_elt = query_elt.elements(data_form.NS_X_DATA, 'x').next()
        except StopIteration:
            raise ValueError # TODO: proper error handling
        else:
            parsed_form = data_form.Form.fromElement(form_elt)
            for col in ('jid', 'description'):
                try:
                    value = parsed_form[col].strip()
                except KeyError:
                    pass
                else:
                    if value:
                        args[col] = value
        
        result = domish.Element((None, 'iq'))
        result['type'] = 'result'
        result['id'] = request['id']
        result['to'] = request['from']
        query_elt = result.addElement('query', NS_SEARCH)
        x_form = data_form.Form('result', formNamespace = NS_SEARCH)
        x_form_elt = x_form.toElement()
        reported_elt = x_form_elt.addElement('reported')
        jid_field_elt = reported_elt.addElement('field')
        jid_field_elt['label'] = 'Jabber ID'
        jid_field_elt['var'] = 'jid'
        description_field_elt = reported_elt.addElement('field')
        description_field_elt['label'] = 'Description'
        description_field_elt['var'] = 'description'

        for row in self._getSubscriptions(args):
            for col, value in zip(('jid', 'description'), row):
                item_elt = x_form_elt.addElement('item')
                field_elt = item_elt.addElement('field')
                field_elt['var'] = col
                value_elt = field_elt.addElement('value', content=value)

        query_elt.addChild(x_form_elt)
        self.xmlstream.send(result)

    def _getSubscriptions(self, args=None):
        """Retrieve the subscriptions to the directory.
        
        @param args (OrderedDict): arguments to filter the request
        @return: list[(boolean, unicode)]
        """
        query = ["SELECT jid, description FROM directory"]

        if args:
            query.append("WHERE")
            query.append(" AND ".join(("%s LIKE ?" % col for col in args)))

        return self.db.execute(' '.join(query), tuple(['%'+arg+'%' for arg in args.values()]))

    def _getSubscription(self, jid):
        """Retrieve the subscription of the given JID.
        
        @param jid (jid.JID): JID to search for
        @return: (boolean, unicode)
        """
        return self._getSubscriptions(OrderedDict({'jid': jid.userhost()})).fetchone()

    def _setSubscription(self, from_jid, subscribed, description):
        """Set the subscription of the given JID.
        
        @param from_jid (jid.JID): JID to subscribe (or unsubscribe)
        @param subscribed (boolean): set to True to subscribe and False to unsubscribe
        @param description (unicode): description
        """
        if subscribed:
            self.db.execute('REPLACE INTO directory(jid,description) VALUES (?,?)', (from_jid.userhost(), description))
        else:
            self.db.execute('DELETE FROM directory WHERE jid=?', (from_jid.userhost(),))
        self.db.connection.commit()

    def onUpdate(self, request):
        """Process the request to update a subscription.
        
        @param request (domish.Element): the update request
        """
        result = domish.Element((None, 'iq'))
        result['type'] = 'result'
        result['id'] = request['id']
        result['to'] = request['from']
        from_jid = jid.JID(request['from'])

        request_cmd = request.elements(NS_COMMANDS, 'command').next()
        command_elt = result.addElement('command', NS_COMMANDS)
        command_elt['node'] = request_cmd['node']

        try:
            session_id = request_cmd['sessionid']
        except KeyError:
            session_id = None

        if session_id is None:
            # first request, we send the form
            command_elt['status'] = 'executing'
            session_id = str(uuid.uuid4())
            actions_elt = command_elt.addElement("actions")
            actions_elt['execute'] = 'complete'
            actions_elt.addElement('complete')
            
            subscription = self._getSubscription(from_jid)
            subscribed, desc = ('true', subscription[1]) if subscription else ('false', '')

            form = data_form.Form('form', title=_('Directory subscription'),
                                  instructions=FORM_INSTRUCTIONS,
                                  formNamespace=NS_SEARCH)
            form.addField(data_form.Field('boolean', 'subscribed', label=_('Subscribed'), values=[subscribed]))
            form.addField(data_form.Field('text-single', 'description', label=_(u"Some words about you"), values=[desc]))
            command_elt.addChild(form.toElement())
        else:
            try:     
                request_form = request_cmd.elements(data_form.NS_X_DATA).next()
            except (StopIteration, KeyError):
                raise ValueError # TODO: properly cancel the command
            parsed_form = data_form.Form.fromElement(request_form)
            subscribed = parsed_form['subscribed'] in ("true", "1")
            description = parsed_form['description']
            self._setSubscription(from_jid, subscribed, description)
            
            command_elt['status'] = 'completed'
            note_elt = command_elt.addElement('note')
            note_elt['type'] = 'info'
            if subscribed:
                note_elt.addContent(SUBSCRIBE_DONE)
            else:
                note_elt.addContent(UNSUBSCRIBE_DONE)

        command_elt['sessionid'] = session_id
        self.xmlstream.send(result)

    def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
        return [disco.DiscoFeature(NS_SEARCH),
                disco.DiscoIdentity(u"directory", u"user", u"salut"),
                disco.DiscoFeature(NS_COMMANDS),
                ID_CMD_LIST]

    def getDiscoItems(self, requestor, target, nodeIdentifier=''):
        ret = []
        if nodeIdentifier == NS_COMMANDS:
            ret.append(disco.DiscoItem(target, "update", "Update your subscription to the directory"))
        return ret