frncscgmz / zeroclickinfo-spice

DuckDuckGo instant answer plugins based on JavaScript APIs

Home Page:http://duckduckhack.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

DuckDuckHack Spice

This documentation walks you through the process of writing a DuckDuckHack Spice plugin. Before reading this section, make sure you've read the DuckDuckHack Intro Site and the DuckDuckHack Developer's Overview (so you know what DuckDuckHack is) and have worked through the Basic tutorial.

Spice Handle Functions

Spice plugins have triggers and handle functions like Goodies, as explained in the Basic tutorial. The difference is that Spice handle functions don't return an instant answer directly like Goodies. Instead, they return arguments used to call a JavaScript callback function that then returns the instant answer.

The JavaScript callback function is defined in another file and is explained in detail in the Spice callback functions section. For now let's concentrate on how it gets called via the Spice handle function.

Usually the Spice plugin flow works like this:

  • Spice plugin is triggered.
  • Spice handle function is called.
  • Spice handle function returns arguments.
  • Arguments are used to make a call to an external JSONP API.
  • The external API returns a JSON object to the Spice callback function.
  • Spice callback function returns instant answer.
  • Instant answer formatted on screen.

The following is an example that calls the Twitter API. Within your zeroclickinfo-spice fork, you would define a similar file in the /lib/DDG/Spice/ directory. This file is named Twitter.pm.

package DDG::Spice::Twitter;

use DDG::Spice;

spice to => 'http://twitter.com/status/user_timeline/$1.json?callback={{callback}}';

triggers query_lc => qr/^@([^\s]+)$/;

handle matches => sub {
    my ($uname) = @_;
    return $uname if $uname;
    return;
};

1;

To refresh your memory, the triggers keyword tells the plugin system when to call a plugin. In the Basic tutorial we discussed using the start keyword to specify trigger words that need to be present at the beginning of the query.

In situations where you want to trigger on sub-words, you can pass a regular expression like in this Twitter example.

triggers query_lc => qr/^@([^\s]+)$/;

The query_lc keyword tells the trigger system to examine a lower case version of the query. The qr/regexp/ construct is the way to specify a compiled regular expression in Perl.

In this case ^@([^\s]+)$ says look for a @ character at the beginning of the query (the ^) and capture (using the parenthesis) everything that isn't a space ( [^\s] ) until you get to the end of the query (the $). Therefore it will match a query like @duckduckgo and capture the duckduckgo part.

The captured parts (matches) get passed to the handle function via the @_ variable (a special Perl array variable).

handle matches => sub {
    my ($uname) = @_;
    return $uname if $uname;
    return;
};

Previously we saw the use of the remainder keyword as in handle remainder, which works well for trigger words. In a case like this one that uses a regular expression trigger, the equivalent is handle matches, which passes the captured parts of the regular expression to the handle function. We look at what was passed and put it into the $uname variable.

    my ($uname) = @_;

If we received a non-blank user name then we return it.

    return $uname if $uname;

Otherwise, return nothing, which short circuits the eventual external call.

   return;

When the username is returned we then plug it into the spice to definition.

spice to => 'http://twitter.com/status/user_timeline/$1.json?callback={{callback}}';

The $uname value from the return statement will get inserted into the $1 placeholder in the spice to line such that you can plug in parameters to the API call as needed. For passing multiple parameters, check out the Advanced spice handlers section.

The {{callback}} template gets plugged in automatically with the default callback value of ddg_spice_twitter. That last part (twitter) is a lowercase version of the plugin name with different words separated by the _ character.

At this point the response moves from the backend to the frontend. The external API sends a JSON object to the callback function that you will also define (as explained in the Spice callback functions section).

Where to go now:

Click to return to the Spice Overview.

Spice Callback Functions

Before reading this section, make sure you've read the basic tutorial, the section on spice handle functions, and the section on testing triggers.

As explained in the Spice handle functions section, a Spice plugin usually calls an external API and returns a JSON object to a callback function. This section explains what that callback function looks like.

Please note: the interface of the callback function is the most beta part of the Spice system, and will be changing soon (for the better). However, you can work away without worrying about what any changes might do to your plugins -- we'll take care of all that.

The callback function is named ddg_spice_plugin_name where plugin_name becomes the name of your plugin. For example, for the Twitter plugin the callback name is ddg_spice_twitter. For multiple word names the CamelCase in the plugin name becomes lower case and separated by _, e.g. HackerNews becomes hacker_news.

Whereas the Spice handle function went in the /lib/DDG/Spice/ directory, the callback function goes in the /share/spice/plugin_name directory. You will need to make that directory. The callback function then gets placed inside a file called spice.js.

Here's a very simple callback function used in the Expatistan Spice at /share/spice/expatistan/spice.js:

function ddg_spice_expatistan(ir) {
    var snippet = '';
    if (ir['status'] == 'OK') {
       snippet = ir['abstract'];
       items = new Array();
       items[0] = new Array();
       items[0]['a'] = snippet;
       items[0]['h'] = '';
       items[0]['s'] = 'Expatistan';
       items[0]['u'] = ir['source_url'];
       nra(items);
    }
}

The end result is a call to the nra function, an internal display function that takes what you send it and formats it for instant answer display.

       nra(items);

We're sending it a JavaScript Array we created called items.

       items = new Array();

The first item in the Array is the main answer. It is another JavaScript Array.

       items[0] = new Array();

An item takes the following parameters.

items[0]['a'] = snippet;

The a param is the required answer. It can be pure HTML in which case it is set via innerHTML. It can also be an object (preferred), in which case onclick and other event handlers won't be destroyed.

The h param is an optional relevant (and relatively short) title.

items[0]['h'] = title;

Source name and URL are required in the s and u blocks. These are used to make the More at X link in all instant answer boxes. Think of it as source attribution.

items[0]['s'] = 'XKCD';
items[0]['u'] = url

An optional image can be passed in the i param. If there is a thumbnail image, we will display it on the right.

items[0]['i'] = image_url

You would usually get the information to make these assignments via the object returned to the callback function. In this case we received it in the ir variable but you can name it anything.

function ddg_spice_expatistan(ir) {

Where to go now:

Click to return to the Spice Overview.

Testing Spice

You should have already tested your Spice triggers by following the Testing triggers section. Once you're confident your triggers are functioning properly, follow these steps to see your Spice plugin on a live server!

Step 1.  Go to the roof of your forked repository.

cd zeroclickinfo-spice/

Step 2.  Start the server.

duckpan server

This command will start up a small Web server running on port 5000 on your machine.

Step 3.  Visit the server in your browser.

You should now be able to go to your duckpan server via a regular Web browser and check it out. It runs code from our site and so generally looks like a real version of DuckDuckGo.

If you're running the duckpan server on the same computer as your Web browser you can navigate to:

http://127.0.0.1:5000/

If you're running the duckpan server on a remote machine, then substitute 127.0.0.1 wither either its IP address or its Fully Qualified Domain Name.

Step 4.  Search.

Given you've already tested your plugin triggers, you should be able to search and see your spice output come through the server. As requests go through the internal Web server they are printed to STDOUT (on the screen). External API calls are highlighted (if you have color turned on in your terminal).

Step 5.  Debug.

If for some reason a search doesn't hit a plugin, there is an error message displayed on the homepage saying "Sorry, no hit for your plugins."

If it does hit and you do not see something displayed on the screen, a number of things could be going wrong.

  • You have a JavaScript error of some kind. Check out the JavaScript console for details. Personally we like to use Firebug internally.

  • The external API was not called correctly. You should be able to examine the Web server output to make sure the right API is being called. If it's not you will need to revise your Spice handle function.

  • The external API did not return anything. Firebug is great for checking this as well. You should see the call in your browser and then you can examine the response.

Step 6.  Tweak the display.

Once everything is working properly (and you have stuff displayed on screen), you will want to mess with your callback function to get the display nice and perfect. Check out the Guidelines for some pointers.

Step 7.  Document.

Finally, please document as much as possible using in-line comments.

Where to go now:

Click to return to the Spice Overview.


Advanced Spice

##Advanced Spice Handlers These advanced handle function techniques are specific to Spice plugins:

Multiple parameters in spice_to call. If you need to substitute multiple parameters into the API call like how the RandWord Spice uses two numbers to specify the min and max length of the random word, you can use from keyword.

spice from => '(?:([0-9]+)\-([0-9]+)|)';

Whatever you return from the handle function gets sent to this spice from regexp, which then gets fed into the spice to API.

spice to => 'http://api.wordnik.com/v4/words.json/randomWord?minLength=$1&maxLength=$2&api_key={{ENV{DDG_SPICE_RANDWORD_APIKEY}}}&callback={{callback}}';

In this case, the two capture blocks will be put into $1 and $2 respectively.

The reason why you do not need to specify a from keyword by default is that the default is (.*), which means whatever you return gets put into $1.

Feeding multiple arguments to spice from. You can have multiple return values in your handle function like the AlternativeTo Spice.

return $prog, $platform, $license;

In this case they are URL encoded and joined together with '/' chars, e.g. in this case $prog/$platform/$license. Then that full string is fed into the spice from regexp.

spice from => '([^/]+)/?(?:([^/]+)/?(?:([^/]+)|)|)';

API Keys. Some APIs require API keys to function properly like in the RandWord Spice. You can insert an API key for testing in the callback function and replace it with a variable reference when submitting.

spice to => 'http://api.wordnik.com/v4/words.json/randomWord?minLength=$1&maxLength=$2&api_key={{ENV{DDG_SPICE_RANDWORD_APIKEY}}}&callback={{callback}}';

You can set the variable when you start duckpan server like this:

DDG_SPICE_RANDWORD_APIKEY=xyz duckpan server

JSON -> JSONP. Some APIs don't do JSONP by default, i.e. don't have the ability to return the JSON object to a callback function. In this case, first you should try to contact the API provider and see if it can be added. Where it cannot, you can tell us to wrap the JSON object return in a callback function like in the XKCD Spice.

spice wrap_jsonp_callback => 1;

Pure JS functions. Sometimes no external API is necessary to deliver the instant answer like how the Flash Version Spice just prints out your Flash Player version using an internal call.

In cases like these you can define a spice_call_type as 'self' like this:

spice call_type => 'self';

Then in the handle function you can return call, e.g.:

return $_ eq 'flash version' ? call : ();

The return of call will run whatever is in the call_type setting. self is a special keyword to just run the callback function directly, in this case ddg_spice_flash_version().

No caching of the external API call. By default, we cache return values from external providers for speed. We use nginx and get this functionality by using the proxy_cache_valid directive. You can override our default behavior by setting your own proxy_cache_valid directive like in the RandWord Spice.

spice proxy_cache_valid => "418 1d";

This is a special declaration that says don't cache. Actually it says cache only 418 HTTP return values for 1 day. Since regular return codes are 200 and 304, nothing will get cached.

If you wanted to say cache those normal values for 1h, you could do:

spice proxy_cache_valid => "200 304 1d";

Advanced Spice Callbacks

In the Spice callback functions section we walked through a simple callback function used in the Expatistan Spice.

Here are some more advanced callback techniques you may need to use:

Setting styles. We use YUI2, which is a JavaScript framework like JQuery. To set styles you can do:

YAHOO.util.Dom.setStyle(obj,'margin-top','5px');

You can also use an id directly like:

YAHOO.util.Dom.setStyle('id','margin-top','5px');

Creating images. We have an internal function for image creation called nur. In the XKCD spice it is used in this construct:

       if (nur) img = nur('',xk['alt'],xk['img']);
       if (img) {

Ignore the first argument. The second is the alt text (title); third is img URL; fourth and fifth optional arguments are explicit height and width to use (in px).

Big images. If you have a big image that may be too big like in the XKCD spice, use the class img_zero_click_big that will resize it appropriately.

You can add classes like this:

YAHOO.util.Dom.addClass(img,'img_zero_click_big');

And again you can pass in an id directly like:

YAHOO.util.Dom.addClass('id','img_zero_click_big');

About

DuckDuckGo instant answer plugins based on JavaScript APIs

http://duckduckhack.com/


Languages

Language:JavaScript 59.2%Language:Perl 38.7%Language:CoffeeScript 2.1%