Technical HOWTOs/en

De Goffiwiki
Aller à : navigation, rechercher


NOTE: this page is outdated, messy and seems wrong sometimes, it needs a cleaning. If you wanna use informations in this page, you can check with us on the SàT room (sat@chat.jabberfr.org)


SàT development (technical HOWTOs)

Triggers

Call a trigger method

The trigger is called from anywhere like that:

host.trigger.point("triggerMethod", param_1, param_2, profile)

The trigger itself would be defined like that:

host.trigger.add("triggerMethod", self.triggerMethod)

[...]

def triggerMethod(self, param_1, param_2, profile):

Note that triggers usually return a boolean to let the caller know if its own processing should be interrupted or not.

Bridge methods

Call a bridge method

Synchronous call

From a local front-end (primitivus, jp...), a bridge method is simply called like that :

host.bridge.bridgeMethod(param_1, param_2, profile=profile)

From Libervia's server side (file libervia.tac) it's almost the same:

profile = ISATSession(self.session).profile
self.sat_host.bridge.bridgeMethod(param_1, param_2, profile)

Libervia's browser side is a bit specific because you cannot use the bridge directly: communication between the browser side and the bridge are done via Libervia's server side. That's why before being able to call the method just like before, you need to add an alias in the BridgeCall class initialization (in libervia.py):

class BridgeCall(LiberviaJsonProxy):
    def __init__(self):
        LiberviaJsonProxy.__init__(self, "/json_api", [..., ..., "bridgeMethodAlias"])

And also define this special method in the MethodHandler class (in libervia.tac):

def jsonrpc_bridgeMethodAlias(self, param_1, param_2):
    profile = ISATSession(self.session).profile
    self.sat_host.bridge.bridgeMethod(param_1, param_2, profile)

Note that the alias doesn't need to be different then the actual bridge method's name. Finally, from Libervia's browser side you call it in the same way as for other frontends but you need to use a callback parameter all the time (you can't have blocking call):

self.host.bridge.bridgeMethodAlias(param_1, param_2, callback=your_callback, profile=profile)

The parameter "callback" that we introduced right after the method's alias will be executed when you get the result from the bridge, eventually taking it as its first parameter.

Important

When you call a bridge method that has been defined in the core, you can name the parameters:

 host.bridge.bridgeMethod(param_2=value_2, profile_key=profile)

But calling a bridge method that has been defined in the plugin, you must not (except for profile and profile_key)! Methods from the plugins are dynamically registered and the bridge doesn't know how the parameters are named, if you use the previous syntax you will get an error. Use the following instead, with no named parameter:

 host.bridge.bridgeMethod(default_value_1, value_2, profile=profile)

Asynchronous call

Now there's also a mechanism to do asynchronous call from Libervia, regardless if the bridge's method is asynchronous itself. Instead of waiting for the answer coming from the bridge, you will defer its result's processing to when it is available. In libervia.tac, instead of:

def jsonrpc_bridgeMethodAlias(self, param_1, param_2):
    profile = ISATSession(self.session).profile
    self.sat_host.bridge.bridgeMethod(param_1, param_2, profile)

You will have:

def jsonrpc_bridgeMethodAlias(self, param_1, param_2):
    profile = ISATSession(self.session).profile
    d = self.asyncBridgeCall("bridgeMethod", param_1, param_2, profile)
    # eventually add a callback here to modify the defered result before returning it
    d.addCallback(lambda d: return d)
    return d

In the browser side your call in the same way as for blocking calls:

self.host.bridge.bridgeMethodAlias(param_1, param_2, callback=your_callback, profile=the_profile)
# something to do

So callback will be executed only when you get the result, but "something to do" is not waiting.

Bridge signals

Observe a bridge signal

DEPRECATED: this section will be obsoleted by new registration process in quickFrontend for SàT 0.6 The signal being emitted from the core, you can register a callback that way:

host.bridge.register("signal", self._signalCallback)

[...]

def _signalCallback(self, contact_jid_s, key, value, profile):
    if not self.check_profile(profile):
        return
    contact_jid = JID(contact_jid_s)

    [...]

Note that when registering your callback, you must respect the number of arguments and expect their types, as defined in the API. DBus handles the following types for the arguments: boolean, integer, string, array... that's why you would get your JID as a unicode string and you probably want to convert it back to a JID object right after the reception.

If the signal is emitted by a plugin, you must specify a third parameter to the registration:

host.bridge.register("signal", self.signalCallback, "plugin")

To observe the signal from Libervia, there's again a specificity. Look for the Libervia class initialization in server.py and add the signal's name in the good loop (there's one for core signal and one for plugin signal):

#core
for signal_name in [..., ..., 'signal']:
    self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name))

or:

#plugins
for signal_name in [..., ..., 'signal']:
    self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin")

Parameters

Foreword

Values and parameters

Sat stores different values which can be general or individual, private or not, binary or not:

  • general: shared between all the profiles of a sat installation
  • individual: specific to one and only one profile
  • private: internal value used by a frontend or a plugin
  • binary: binary value that is not human-readable

Examples:

  • the IP address of the machine is stored as a general value
  • a password or any option left to the user are stored as individual values
  • the default profile and the avatar hashes are stored as general and private values
  • the index of the IMAP folder is stored as an individual, private and binary value

In this section, we will call "parameter" the general or individual values that are not private nor binary. This basically restricts the term "parameter" to the values that are relevant for the user.

Core and plugin parameters

Parameters can be defined statically by the core or dynamically by a plugin:

  • core parameters are defined in sat/src/memory/memory.py, class Params, attribute "default_xml"
  • plugin parameters can be defined in each plugin class, in the attribute "params"

Add a plugin parameter

To add a plugin parameter, insert and adapt the "params" attribute to your plugin class, then call "importParams" in the __init__ method:

params = """
<params>
  <general>
    <category name="%(category_name)s" label="%(category_label)s">
      <param name="%(param1_name)s" label="%(param1_label)s" value="true" type="bool"/>
      <param name="%(param2_name)s" label="%(param2_label)s" value="%(param2_default)s" type="list" security="0">
        <option value="%(param2_option1)s"/>
        <option value="%(param2_option2)s"/>
        <option value="%(param2_option3)s"/>
      </param>
    </category>
  </individual>
</params>
""" % {
       'category_name': "<category>",
       'category_label': _("<category>"),
       'param1_name': "<name1>",
       'param1_label': _("<name1>"),
       'param2_name': "<name2>",
       'param2_label': _("<name2>"),
       'param2_default': _("<option1>"),
       'param2_option1': _("<option1>"),
       'param2_option2': _("<option2>"),
       'param2_option3': _("<option3>")
}

[...]

def __init__(self, host, [...]):
    [...]
    host.memory.importParams(self.params)
    [...]
  • Note the security attribute which will be checked against the frontend security limit to allow or disallow the parameter access. The security attribute can be omitted or set to -1 (default value) for a maximal security. A value of 0 means "good security", where greater values are less and less secure. But for now sole -1 and 0 are used. Local frontends have a security limit of -1, that means no security limit, so they can access all security levels. Libervia has a security limit of 0, allowing it to access all security levels greater or equal to zero. So the security attribute should be omitted or set to -1 (default value) for a parameter that should not be accessible from Libervia, or to 0 if the parameter can be safely retrieved and modified from anywhere.
  • Parameters types "string" and password are also supported.
  • For convenience, you could also set the list option like that:
options = [_("<option1>"), _("<option2>"), _("<option3>")]
params = """
[...]
      <param name="%(param2_name)s" label="%(param2_label)s" value="%(param2_default)s" type="list" security="0">
        %(param2_options)s
      </param>
[...]
""" % {
       [...]
       'param2_default': options[0],
       'param2_options': ['<option value="%s"/>' % value for value in options]
}

Retrieve the parameter list

You can call through the bridge one of the core method getParams, getParamsForCategory or getParamsUI.

def onParam(self):
    def success(params):
        [...]
    def failure(error):
        [...]
    self.bridge.getParams(profile_key=self.profile, callback=success, errback=failure)

There's an optional parameter security_limit (not visible here) which is used to restrict the result to a certain set of parameters. The default value of -1 means that all the parameters are returned (which is usually the case for local frontends), otherwise sole the parameters which are defined with a security level lower or equal to security_limit are returned. Libervia, as a web frontend, needs more attention and you do like that:

   def onParam(self):
       def gotParams(params):
           [...]
       self.host.bridge.call('getParams', gotParams)

And in libervia.tac file, add a new method to the MethodHandler class:

   def jsonrpc_getParams(self):
       profile = ISATSession(self.session).profile
       d = defer.Deferred()
       # SECURITY_LIMIT is defined to 0 on top of the file
       self.sat_host.bridge.getParams(SECURITY_LIMIT, profile, callback=d.callback, errback=d.errback)
       return d

Note: a deferred object is used here to make the request asynchronous. More information on the twisted website.

Plugins

Create a plugin to implement a new feature

Create a in the src/plugins directory a new file called plugin_???.py and use the following template (replace ??? anywhere you would find it). It gives a sample use case of the trigger, bridge method and signal mechanisms.

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

# SAT plugin for ??? Protocol (xep-0???)
# Copyright (C) 2009, 2010, 2011, 2012, 2013 ??? ??? (???@???.???)

# 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 import exceptions
from logging import debug, info, error
from wokkel import disco, iwokkel
from zope.interface import implements
from twisted.words.protocols.jabber.jid import JID
try:
    from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
    from wokkel.subprotocols import XMPPHandler


PLUGIN_INFO = {
    "name": "??? Protocol Plugin",
    "import_name": "XEP-0???",
    "type": "XEP",
    "protocols": ["XEP-0???"],
    "dependencies": [],
    "main": "XEP_0???",
    "handler": "yes",
    "description": _("""Implementation of ??? Protocol""")
}


# Define here your global variables


class XEP_0???(object):
    """
    Implementation for XEP 0???
    """
    params = """
    <params>
    <individual>
    <category name="%(category_name)s" label="%(category_label)s">
        <param name="%(param_name)s" label="%(param_label)s" value="true" type="bool" security="0"/>
     </category>
    </individual>
    </params>
    """ % {
        'category_name': ???,
        'category_label': _(???),
        'param_name': ???,
        'param_label': _('Enable ???')
    }

    def __init__(self, host):
        info(_("??? plugin initialization"))
        self.host = host

        host.memory.importParams(self.params)

        # add a trigger method to be called from elsewhere
        host.trigger.add("triggerMethod", self.triggerMethod)

        # add a method to be called from the frontends
        host.bridge.addMethod("pluginMethod", ".plugin", in_sign='ss',
                              out_sign='', method=self.pluginMethod)

        # add a signal to be broadcasted
        host.bridge.addSignal("pluginSignal", ".plugin", signature='ss')

    def getHandler(self, profile):
        return XEP_0???_handler(self, profile)

    def triggerMethod(self, param_1, param_2, profile):
        """
        The method signature must fit its call. Triggers are usually
        called to perform a check or an operation, and they can report
        any problem to the caller by returning False. For example the
        present method, checking if param_1 is greater than param_2,
        can be called from the core like that:

        if not self.host.trigger.point("triggerMethod", param_1, param_2, profile):
            # trigger returned false, abort the processing
            return

        """
        return param_1 > param_2

    def pluginMethod(self, param_1, profile):
        """
        This method can be called from anywhere like that:

        self.host.bridge.pluginMethod(param_1, profile)

        Here we emit a signal that can be catched after being declared:

        self.host.bridge.register("pluginSignal", self.signalCallback, "plugin")

        [...]

        def signalCallback(self, param_1, profile):
            # what to do when the signal is emitted
            return

        """
        self.host.bridge.pluginSignal(param_1, profile)
        return


class XEP_0???_handler(XMPPHandler):
    implements(iwokkel.IDisco)

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

    def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
        return [disco.DiscoFeature(<??? namespace for the implemented feature>)]

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