15 KiB
DuckDuckHack Goodies
This documentation walks you through the process of writing a DuckDuckHack Goodie plugin. Before reading this section, make sure you've read the DuckDuckHack Intro Site and the DuckDuckHack Developer's Overview (so you know what we're talking about).
Basic Tutorial
In this tutorial, we'll be making a Goodie plugin that checks the number of characters in a given search query. The end result will look like this and works like this. The same framework is used to trigger Spice plugins.
Let's begin. Open a text editor like gedit, notepad or emacs and type the following:
package DDG::Goodie::Chars;
# ABSTRACT: Give the number of characters (length) of the query.
Each plugin is a Perl package, so we start by declaring the package namespace. In a new plugin, you would change Chars to the name of the new plugin (written in CamelCase format).
The second line is a special comment line that gets parsed automatically to make nice documentation (by Dist::Zilla).
Next, type the following use statement to import the magic behind our plugin system.
use DDG::Goodie;
A Note on Modules
Right after the above line, you should include any Perl modules that you'll be leveraging to help generate the answer. Make sure you add those modules to the dist.ini file in this repository. If you're not using any additional modules, carry on!
Now here's where it gets interesting. Type:
triggers start => 'chars';
triggers are keywords that tell us when to make the plugin run. They are trigger words. When a particular trigger word is part of a search query, it tells DuckDuckGo to trigger the appropriate plugins.
In this case there is one trigger word: chars. Let's say someone searched "chars this is a test." chars is the first word so it would trigger our Goodie. The start keyword says, "Make sure the trigger word is at the start of the query." The => symbol is there to separate the trigger words from the keywords (for readability).
Now type in this line:
handle remainder => sub {
Once triggers are specified, we define how to handle the query. handle is another keyword, similar to triggers.
You can handle different aspects of the search query, but the most common is the remainder, which refers to the rest of the query (everything but the triggers). For example, if the query was "chars this is a test", the trigger would be chars and the remainder would be this is a test.
Now let's add a few more lines to complete the handle function.
handle remainder => sub {
return 'Chars: ' . length $_ if $_;
return;
};
This function (the part within the {} after sub) is the meat of the Goodie. It generates the instant answer that is displayed at the top of the search results page.
Whatever you are handling is passed to the function in the _** variable ( **
_ is a special default variable in Perl that is commonly used to store temporary values). For example, if you searched DuckDuckGo for "chars this is a test", the value of $_ will be "this is a test", i.e. the remainder.
Let's take a closer look at the first line of the function.
return 'Chars: ' . length $_ if $_;
The heart of the function is just this one line. The remainder is in the _** variable as discussed. If it is not blank ( **if
_ ), we return the number of chars using Perl's built-in length function.
Perl has a lot of built-in functions, as well as thousands and thousands of modules available via CPAN. You can leverage these modules when making Goodies, similar to how the Roman Goodie uses the Roman module.
If we are unable to provide a good instant answer, we simply return nothing. And that's exactly what the second line in the function does.
return;
This line is only run if $_ contained nothing, because otherwise the line before it would return something and end the function.
Now, below your function type the following line:
zci is_cached => 1;
This line is optional. Goodies technically return a ZeroClickInfo object (abbreviated as zci). This effect happens transparently by default, but you can override this default behavior via the zci keyword.
We set is_cached to true (0 is false, 1 is true) because this plugin will always return the same answer for the same query. This speeds up future answers by caching them (saving previous answers).
Finally, all Perl packages that load correctly should return a true value so add a 1 on the very last line.
1;
And that's it! At this point you have a working DuckDuckHack Goodie plugin. It should look like this:
package DDG::Goodie::Chars;
# ABSTRACT: Give the number of characters (length) of the query.
use DDG::Goodie;
triggers start => 'chars';
handle remainder => sub {
return 'Chars: ' . length $_ if $_;
return;
};
zci is_cached => 1;
1;
Review
The plugin system works like this at the highest level:
-
We break the query (search terms) into words. This process happens in the background.
-
We see if any of those words are triggers (trigger words). These are provided by each of the plugins. In the example, the trigger word is chars.
-
If a Goodie plugin is triggered, we run its handle function.
-
If the Goodie's handle function outputs an instant answer via a return statement, we pass it back to the user.
Where to go from here
If you're planning on writing a Goodie plugin:
Before heading to the sections below, jump on over to the page that covers testing triggers. See you back here soon!
"I came here to write Spice!":
Cool! You're done the basic tutorial. Now check out the section on Spice handle functions in the zeroclickinfo-spice repository.
Advanced Triggers
In the Basic tutorial we walked through a one word trigger and in the Spice handle functions section we walked through a simple regexp trigger.
Here are some more advanced trigger techniques you may need to use:
Multiple trigger words. Suppose you thought that in addition to chars, numchars should also trigger the Chars Goodie. You can simply add extra trigger words to the triggers definition.
triggers start => 'chars', 'numchars';
Trigger locations. The keyword after triggers, start in the Chars example, specifies where the triggers need to appear. Here are the choices:
- start - just at the start of the query
- end - just at the end of the query
- startend - at either end of the query
- any - anywhere in the query
Combining locations. You can use multiple locations like in the Drinks Spice.
triggers any => "drink", "make", "mix", "recipe", "ingredients";
triggers start => "mixing", "making";
Regular Expressions. As we walked through in the Spice handle functions section you can also trigger on a regular expression.
triggers query_lc => qr/^@([^\s]+)$/;
We much prefer you use trigger words when possible because they are faster on the backend. However, in some cases regular expressions are necessary, e.g. when you need to trigger on sub-words.
Regexp types. Like trigger words, regular expression triggers have several keywords as well. In the above example query_lc was used, which operates on the lower case version of the full query. Here are the choices:
- query_raw - the actual (full) query
- query - with extra whitespace removed
- query_lc - lower case version of the query and extra whitespace removed
- query_clean - lower case with non alphanumeric ASCII and extra whitespace removed
- query_nowhitespace - with whitespace totally removed
- query_nowhitespace_nodash - with whitespace and dashes totally removed
If you want to see some test cases where these types are enumerated check out our internal test file that tests they are generated properly.
Two-word+ triggers Right now trigger words only operate on single words. If you want to operate on a two or more word trigger, you have a couple of options.
- Use a regular expression trigger like in the Expatistan Spice.
triggers query_lc => qr/cost of living/;
- Use single word queries and then further qualify the query within the handle function as explained in the Advanced handle functions section.
Advanced Handle Functions
In the Basic tutorial we walked through a simple query transformation and in the Spice handle functions section we walked through a simple return of the query.
Here are some more advanced handle techniques you may need to use:
Further qualifying the query. Trigger words are blunt instruments; they may send you queries you cannot handle. As such, you generally need to further qualify the query (and return nothing in cases where the query doesn't really qualify for your goodie).
There are number of techniques for doing so. For example, the first line of Base Goodie has a return statement paired with unless.
return unless /^([0-9]+)\s*(?:(?:in|as)\s+)?(hex|hexadecimal|octal|oct|binary|base\s*([0-9]+))$/;
You could also do it the other way, like the GoldenRatio Goodie.
if ($input =~ /^(?:(?:(\?)\s*:\s*(\d+(?:\.\d+)?))|(?:(\d+(?:\.\d+)?)\s*:\s*(\?)))$/) {
Another technique is to use a hash to allow specific query strings, as the GUID Goodie does.
my %guid = (
'guid' => 0,
'uuid' => 1,
'globally unique identifier' => 0,
'universally unique identifier' => 1,
'rfc 4122' => 0,
);
return unless exists $guid{$_};
Handling the whole query. In the Chars example, we handled the remainder. You can also handle:
- query_raw - the actual (full) query
- query - with extra whitespace removed
- query_parts - like query but given as an array of words
- query_nowhitespace - with whitespace totally removed
- query_nowhitespace_nodash - with whitespace and dashes totally removed
For example, the Xor Goodie handles query_raw and the ABC Goodie handles query_parts.
Using files. You can use simple text/html input files for display or processing.
my @words = share('words.txt')->slurp;
The Passphrase Goodie does this for processing purposes and the PrivateNetwork Goodie does it for display purposes.
The files themselves go in the /share/goodie/ directory.
Generating data files. You may also need to generate data files. If you do so, please also include the generation scripts. These do not have to be done in Perl, and you can also put them within the /share/goodie/ directory. For example, the CurrencyIn Goodie uses a Python script to generate the input data.
There are a couple more sections on advanced handle techniques depending on Plugin type:
- For Goodies, check out the Advanced Goodies section.
- For Spice, check out the Advanced Spice handlers section.
Advanced Goodies
These advanced handle techniques are specific to Goodie plugins:
Returning HTML. Goodies return text instant answers by default, but can return simple HTML as well. In that case, simply attach the html version to the end of the return statement.
return $text, html => $html
Other zci keywords. The Chars example sets the is_cached zci keyword. You can find other settable attributes in the object documentation. For example, the GoldenRatio Goodie sets the answer_type variable, which gets returned in the API.
zci answer_type => "golden_ratio";
Location API
Sometimes, all a plugin needs is the user's location. This is where the Location API comes in. An example is the Is it snowing? plugin:
# Phoenixville, Pennsylvania, United States
my $location = join(" ", $loc->city . ', ', $loc->region_name . ', ', $loc->country_name);
When testing on duckpan
, the plugin will always point you to "Phoenixville, Pennsylvania, United States," but don't worry, because it will show the real location once it's live.
And it isn't limited to just the city, the state, and the country, either. Location.pm lists all the things that you can possibly use:
my @geo_ip_record_attrs = qw( country_code country_code3 country_name region
region_name city postal_code latitude longitude time_zone area_code
continent_code metro_code );