Sep
07
I finally sat down and did something for botzilla that I’ve been meaning to do
for a long time, a multi-step Dialog. No branches or question requirements yet,
but I’m pleased enough with my first found use for it that I feel it’s releasable.
Dialog is an “outside” class (not a ziggi) that sits in botzilla/cls/ that ziggi
plugins can make use of to pose a series of questions to a user and store their
responses.
There are several things that need setting up to have a smooth ride, so let me
begin my going over the pieces. The individual class files are more detail on how
they can be used.
* A ziggi will be used in between the human and the Dialog, where I tried to make
it as easy as possible (though a little harder than your normal ziggi).
* Dialog is the base class for generic dialogs. Ideally nothing should have to
change here unless new features like branching are added.
* Dialogs use DialogQuestionSets for their series of questions. When our ziggi
loads for the first time, it will create a Dialog that implements a question set
that user’s can respond to.
* These question sets use DialogQuestions to pose a query to the user. These are
little more than an object with an array of attributes, including space for a
callback function and a validation fail message.
* When a user begins a dialog, they’ll spawn a unique DialogForm, which is an
instance of a user going through the question set.
Each of these classes are in a their own files.
–
Dialog begins with a series of default commands prefixed by a command character.
I made this command character different than botzilla’s normal one, but I made
it very easy to change it to whatever you wanted it to be. Along with the
prefix char, the characters that call individual commands can be overriden. Those
commands are (semantically) “begin”, “quit”, “next”, “prev”, and “help” -> (respectively by default),
“:start”,”:x”,”:>”,”:<”,”:?” . The whole idea around the prefix char it so allow
users to hold separate conversations in a channel and leave the dialog running
without it thinking that it’s being responded to; you may very well drop it if you want.
The base DialogQuestionSet has some built in validators/processors that questions
can call on their responses. For instance, if your question is looking for a
number, the callback can return a fail message if it doesn’t like the answer.
The question is then not yet satisfied.
We’ll start by making a ziggi that implements Dialog. The package includes an example file. For this example, I’ll make
a form that polls favorite browsers and web apps.
class browser_dialog extends ziggi
{
function browser_dialog()
{
$this->d = new Dialog();
$this->d->setCommand('begin','browser');
$this->d->setStoragePath(PERM_DATA.'browser/');
$this->d->setFilePrefix('browser');
/* add question set */
}
}
First off, Dialog is a member of this object. On the next line, I’m overriding the
default “begin” command with the command “browser”, making the dialog startable when
a user says “:browser”. This is mostly to avoid conflict with any other Dialogs that
might be available at the time. setCommand() accepts a command:word pair or
an array of those to do multiple replacements.
Next, I’m setting a storage path in botzilla’s file system to save a response
file for this user. Setting a file prefix prepends the filename so that the
space could be shared among other files with different purposes. For this example,
my response file for this Dialog is predetermined to be botzilla/perm_data/browser/browser_bibby
Or course the prefix is optional. If the storage path is a directory that doesn’t exist,
an attempt is made to create it.
It is not Dialog’s job to know what to do with these files for purposes other
than creating them (ie, Dialog isn’t going to report on what browsers people use).
Even so, if a user repeats the same Dialog, their previous responses are recalled
and shown to the user again. Those questions are automatically “satisfied”, and
skipping the quesiton doesn not remove the first response, but changes/updates are possible.
In the code above, where I’ve commented “add question set”, I’m going to create a question
set and apply it to the Dialog. QuestionSet’s addQuestion method will
take one DialogQuestion object or an array of those.
DialogQuestion constructors accept an array for an argument; that array containing settings/overrides
for the question itself. Available keys/attributes are:
QTEXT // string, question to ask user
FIELD // string, storage field name to pair to the answer in the concluded output
ACCEPT // string, QuestionSet method name to validate response
UNACCEPTED // string, message to tell user that they answered wrongly
ACCEPTED // string, message to tell user that they answered rightly
REQUIRED // bool, the form can complete wihout a response (not implemented)!
Each of these pretty much default to null, so to have an interesting and useful
Dialog, you’re probably going to want to set QTEXT and FIELD. If you want any
trimming or other processing or validation, name a method of DialogQuestionSet
here as a string. Feel free to extend DialogQuestionSet and add methods to it
if you want to do something that the base class doesn’t do. Validators should
return FALSE if it doesn’t like the answer.
It’s perfectly acceptable to return an array for a response from a callback. Really,
if it isn’t an array, it soon will be. For consistency sake, all responses are
stored as arrays. In my first Dialog, I am exploding the user’s response on spaces
and commas asking for variations of a single item (all your IRC handles). Multiple
answers in a single response is automatically handled by the response handler.
// still in the constructor $qset = new DialogQuestionSet(); $qset->addQuestion(array ( new DialogQuestion(array( 'QTEXT'=>'Welcome to this Dialog. Remember to begin your response with \''.DIALOG_CHAR.'\' so that I know you\'re talking to me. So, how many different browsers to use on regular basis? (multiple versions of the same product may be counted as different browsers)', 'FIELD'=>'NUM_BROWSERS', 'ACCEPT'=>'number', 'UNACCEPTED'=>'Sorry, not a number. Try again.', )), new DialogQuestion(array( 'QTEXT'=>'Did the Google Chrome EULA scare you like it scared me? (I hear they\'re fixing that)', 'FIELD'=>'CHROME_SCARE', 'ACCEPT'=>'yesno' )), new DialogQuestion(array( 'QTEXT'=>'What's your favorite browser?', 'FIELD'=>'FAV_BROWSER', 'ACCEPT'=>'trim' )), new DialogQuestion(array( 'QTEXT'=>'Loosely name for me the web technologies you\'re interested in', 'FIELD'=>'WEB_TECH', 'ACCEPT'=>'split' )), ));
When answered, this form may produce an output file similar to this: (note that “split” responses are put on different lines for easier reading)
NUM_BROWSERS::3 CHROME_SCARE::TRUE FAV_BROWSER::Firefox 3.1 WEB_TECH::javascript WEB_TECH::knickers WEB_TECH::tracemonkey
ACCEPT can also be an array of multiple callbacks letting you further process the input.
Now, if you’ve made a ziggi for your botzilla, you know that the method parseBuffer is the most
important, and is called every time the bot hears anything. Dialog’s natively have their
own blank ziggi instance to use it’s functions to read the buffer. So to do this right,
we’re going to pass the raw buffer directly to the Dialog and listen for a response just
as botzilla does with other ziggis.
When Dialog returns a string or an array, that data is printable back to the user.
When a form is officially complete, Dialog will send back the DialogForm object itself.
Having this object data type is easy to catch, and provides you the opportunity to
do something else with the responses other than store it in a file.
// in your ziggi class
function parseBuffer()
{
if($this->isEmpty()) // no sense wasting time.
return;
if(is_a($this->d,'Dialog'))
{
// pass the buffer and listen out.
$r = $this->d->parseBuffer($this->bz_buffer);
// say what needs to be said
if(is_array($r) || is_string($r))
$this->pm($r);
//this happens when the form is complete
if(is_a($r,'DialogForm'))
{
//store the data and finish
$this->pm( $this->d->conclude($r->user,TRUE) );
}
}
return false;
}
Dialog->conclude finishes and destroys the user form. The second argument is a boolean to
say “store it to a file” or not. When the “response” ($r here) is the DialogForm, you can get the responses out with
$r->getResponses() or accessing $r->responses (however the member responses is a numeric array without FIELD names).
With those, you can pretty much do what you please with the data. It’s a good habit to
destroy any unneeded forms once their finished to free up memory.
For my initial purpose though, I needed something to ask a series of questions and store the answers.
A separate class then reads those files, makes sense of them, and provides utility for any ziggi that wants to use it.
“Outside” classes like that are not unwelcome; in fact whenever your beginning to think that ziggis should share function,
the solution usually is to provide an outside class that they both use.
Finished ziggi:
class browser_dialog extends ziggi
{
function browser_dialog()
{
$this->d = new Dialog();
$this->d->setCommand('begin','browser');
$this->d->setStoragePath(PERM_DATA.'browser/');
$this->d->setFilePrefix('browser');
$qset = new DialogQuestionSet();
$qset->addQuestion(array
(
new DialogQuestion(array(
'QTEXT'=>'Welcome to this Dialog. Remember to begin your
response with \''.DIALOG_CHAR.'\' so that I know you\'re
talking to me. So, how many different browsers to use on
regular basis? (multiple versions of the same product may
be counted as different browsers)',
'FIELD'=>'NUM_BROWSERS',
'ACCEPT'=>'number',
'UNACCEPTED'=>'Sorry, not a number. Try again.',
)),
new DialogQuestion(array(
'QTEXT'=>'Did the Google Chrome EULA scare you like
it scared me? (I hear they\'re fixing that)',
'FIELD'=>'CHROME_SCARE',
'ACCEPT'=>'yesno'
)),
new DialogQuestion(array(
'QTEXT'=>'What's your favorite browser?',
'FIELD'=>'FAV_BROWSER',
'ACCEPT'=>'trim'
)),
new DialogQuestion(array(
'QTEXT'=>'Loosely name for me the web technologies you\'re interested in',
'FIELD'=>'WEB_TECH',
'ACCEPT'=>'split'
)),
));
}
function parseBuffer()
{
if($this->isEmpty()) // no sense wasting time.
return;
if(is_a($this->d,'Dialog'))
{
// pass the buffer and listen out.
$r = $this->d->parseBuffer($this->bz_buffer);
// say what needs to be said
if(is_array($r) || is_string($r))
$this->pm($r);
//this happens when the form is complete
if(is_a($r,'DialogForm'))
{
//store the data and finish
$this->pm( $this->d->conclude($r->user,TRUE) );
}
}
return false;
}
}
I hope to be able to expand the functionality of this feature (or meet someone
that’d like to share something similar), but chances are that if from me, it’d
take quite a while to get around to. I submit this as-is (working) with the
hopes that someone find it useful.