Herein you will find enough information to write basic JobyX modules, and/or get JobyX running. If you need any other assistance, visit the SourceForge project page at http://www.sourceforge.net/projects/jobyx.
The file jobyxconfig.pm contains global constants that JobyX uses to login to the server, load default modules, etc. A sample is provided, called jobyxconfig.pm.demo. Rename this to jobyxconfig.pm and edit it to suit your needs. Note that you should remove the sample language customization constants, unless you want JobyX to parse shouts instead of tells.
For information on customizing what you call your TUs (Trusted Users), see the top of JX_Mod/CORE/English.pm. It will explain how to do this by editing jobyxconfig.pm.
I have tried to supply meaningful default command levels for each command. Feel free to tweak them. They are located in JX_Mod/CORE.pm. After you modify them, back up the file lest a JobyX update clobber your modified version. This also means you will have to re-edit them after a JobyX update. The backup you made will help you remember which settings you chose.
Modules should be placed in path_to_jobyx/JX_Mod/
and should have a package name of JX_Mod::your_module_name
. For example, my Tourney module is placed in /home/chris/jobyx/JX_Mod/Tourney.pm and the first line is package JX_Mod::Tourney;
. Note that you may not create nested modules; JX_Mod::Tourney::Seek
won't work.
Every user has a numeric userlevel, which defaults to 10, unless the user is a guest, then the level is 1. Userlevels can range from 0-100. All commands should be above level 0, as this level is reserved for abusers. Userlevels can be customized by editing the @levels
definition in jobyx.pl. The following levels should ALWAYS be in place - modification could cause problems.
0 = Abuser
1 = Guest
10 = Player
99 = Operator
Any level defined as 100 will be ignored, as it is reserved for JobyX itself. On startup it will automatically set level 100 to the bot's server handle.
You may modify the names of the default levels, but make sure they still mean the same thing. For example, 0 could be ``Offender'', 1 could be ``Unreg'', 10 could be ``User'', and 99 could be ``Super User''.
----------------
This is experimental and subject to change. In the future, requirements will be tested via a verification sub. For example:
sub verify_insmod { my @ui=&::userinfo($_[0]); $ui[0] or return 0; return ($ui[1]->[4] >= 99 or &::inlist('tadmin',$_[0])); }
While a bit clunkier, this will allow more complicated checks, as well as a cleaner display of ListCommands.
Note that if only a userlevel check is needed, a number may be used in place of a verification sub. This level would also be displayed on the ListCommands output. If a verification sub is used, the userlevel will not be displayed on this output.
----------------
To restrict access to commands by userlevel or position in lists, a (sort of) language has evolved. Here are the possible arguments:
<list>
list.
<list>
list.
You can combine these with the following ``operators'':
Operators are read from left to right. There is no ``order of operations''.
Note that parentheses may not be used for grouping! If you need to do something really complex, verify it in the verification/command subroutine.
Any time that ``userlevel'' is mentioned, it really means ``qualifications string'' unless it says ``numeric userlevel''.
The core provides several subroutines for common variable/list types. Note that some have no application for a list. For example, &::boolean probably wouldn't make a good list validator. To create your own verification subs, use either of the following formats:
Note that it is not required to use any parameters. For example, &::handle
only uses $new_item
(or $new_setting
for variables) to validate the item. On the other hand, &::boolean
uses $old_setting
when $new_setting
is null. It is possible to create subs for lists and variables, like &::handle
or &::notnull
.
Because JobyX uses a nonblocking socket, detecting when the server closes the connection is not as easy as waiting for input to run out. In order to detect a broken connection, JobyX will examine each line against the regular expressions found in @Lang::CORE::connection_fail
.
However, JobyX anticipates that your connection may not always be cleanly cut, so if it doesn't recieve anything from the server for one full minute, it will send the bogus ``notacommand'' command. If JobyX doesn't recieve a reply within 30 seconds, it assumes that the connection was dropped, saves all data, and restarts itself.
Commands are the heart of JobyX's operation. When JobyX is told something, it will attempt to locate the best match from the commands it knows, and pass execution to the appropriate subroutine.
@command=(
!helloworld
, nobody will be able to use it, because the command parser will try to execute the ``!'' command with ``helloworld'' as the first argument.
&handler($handle_of_caller,@args)
, where $handle_of_caller
is the handle of the user that is executing the command, and @arg
is the arguments supplied with the command.
$visible==0
, but you should still supply it so you don't confuse yourself.
);
JobyX comes with a standard ICS-compatible list feature. Lists can be used to store a variety of information including handles, IP addresses, email addresses, port numbers, ``bad words'', and anything else you can think of.
@list=(
&verifysub($handle_of_caller,$list_name,$item)
and will expect a return list of:
return (1) #item is OK, add it return (2,$item) #add $item instead return (0,$error) #don't add the item, and give the error #$error to the caller.
The (2,$item)
format may be used to add a different item instead.
If anything is acceptable, set the reference to \&::notnull
.
&addsub($handle_of_caller,$list_name,$item,$add) # note that &subsub has the same parameters # $add == 1 if the item was added
A return value of true will qtell $item
the You have been added to/removed from the $list_name
list by $handle_of_caller
. message. If you don't want to write a handler just to return true, set the reference to \&::true
. You should definately not return true if the list contains something other than handles. To make the list automatically add a comment for the user, set the reference to \&::list_comment
, which also returns true, meaning that the user will also be notified.
This can be employed to do a variety of things. For my Tourney module, I use it to call \&::setlevel
to change the user's level to 20 when $list_name
eq Manager, providing that the user is below level 20. I could also make it &::send("tell 6 $item has been made a TM by $handle_of_caller.\n")
, though that could be overkill.
\&addsub
for details.
$listname
list: $num $type
--. For example, lists that store handles should stick with ``names''; lists that store IP addresses may want to use ``ips''; etc.
);
Variables are a simple, yet effective way for each user to keep their own settings. First, check to see if a personal list would be better for the option you're considering. For example, ``censor'' probably wouldn't make a good variable; a personal list would be more efficient.
@variable=(
$havelv
is 20, thus preventing non-TMs from getting confused about it.
0 = Everybody can see the setting. 1 = Each user can only see his/her own setting. 2 = Nobody can see the setting.
&verifysub($handle_of_caller,$variable,$setting)
and expects a return value of:
return (1) # setting is OK, set it return (2,$setting) # set to $setting instead return (0,$error) # setting is not OK. use error $error # if present, else use generic "Bad # setting for variable $variable."
If the variable is a standard boolean (on/off) variable, use \&::boolean
.
&setsub($handle_of_caller,$variable,$setting)
In my Tourney module, I use &::send
to add or remove the user from the server's TM list depending on the $setting
of the Open variable. This will turn their (TM) light on or off.
Note that you can use different subs for different variables, or the same sub for a few or all variables. It's up to you how you want to use it. $variable
is supplied specifically for this case. If you use different subs for different variables, you can safely ignore it.
);
Avariables are in a sense JobyX's personal variables. They should be used to hold things that affect JobyX's behavior globally. Like variables and personal lists, consider whether a public list would be better suited. In my BadWord module, for example, I use a global list, BadWords, to store the list of unacceptable terms. However, the same module uses the MaxWarnings avariable to set how many warnings a user can receive before he/she is automatically muzzled. The ASet command is used to modify avariables, and you may view them by using AVariables.
@avariable=(
\&verifysub
definition under Variable Definition.
It is probably safe to use \&::notnull
here, since only people in the aconfig list can use it, and that will most likely be just you and a few select others that (supposedly) know what they are doing. If you plan on having a lot of (clueless) people in this list, you may wish to define verification subs.
&setsub($handle_of_caller,$avariable,$setting)
You probably won't need to use this one, but it is provided for flexibility.
);
When a module is inserted, its ICS_init
sub will be called like:
&JX_Mod::<module>::ICS_init($username,$JobyX_version,$handle_of_insertee)
$username
is JobyX's handle on the server, and $JobyX_version
is the version of JobyX in the format major.minor.revision. You can use this to check for a particular version. $handle_of_insertee
is the server handle of the user that inserted the module. Note that the module may be loading before the server connection is established, so don't do anything requiring a connection. Your module's ICS_begin
sub will be called (without any parameters) after the connection to the server is complete. If a bot operator inserts the module, ICS_begin
will be called immediately following ICS_init
.
The real meat of this subroutine is the return value. This is your chance to tell JobyX about any commands, lists, variables, and avariables your module provides.
return ( [\@command1 ... \@commandN], [\@list1 ... \@listN], [\@variable1 ... \@variableN], [\@avariable1 ... \@avariableN], \&idlesub );
Notice that the actual commands/lists/etc are references. This is very important! The syntax of the @command/@list/etc lists are specified above.
Returning (0,$error)
will abort the load. I use this in my Tourney module to abort a load when a stored tourney can't be read.
\&idlesub
will be called when there is no input from the server. This means that your sub could get called as many as five times per second! This sub should be quick and efficient--too much work could delay JobyX's response, as it will not begin reading from the server again until this function returns. In my Tourney module, I use it to periodically announce tourneys. Like most other subroutine references, you may supply undef
if you're not going to use it.
If one of your modules depends on another, use &::ismoduleloaded($module)
to determine if a required module is loaded. Generally, modules should not be dependant on other modules. If you think two modules need each other to function, consider merging them in to one module. This will be cleaner and more efficient.
Also note that you can't specify what number the module will have. More on this in Conflicts.
When a module is about to be removed, the ICS_term
sub will be called like:
&Module::ICS_term($handle_of_remover)
$handle_of_remover
is the server handle of the user that removed the module. If the handle is *DEBUG*, JobyX itself unloaded the module, probably during a shutdown/restart.
Like ICS_init
, you can return stuff here:
return (0) #All OK, remove the module return (1,$error) #Something's wrong, don't remove the module, #and tell the remover $error.
In my Tourney module, I return (1,$error)
when I can't save the tourney list, for example. If the error is not serious, it is a good idea to return (0)
. Note that returning (1,$error)
will not cancel the unload if the connection to the server was terminated, or if a bot operator forced a shutdown/restart.
This sub should be used to save any persistant items. Note that variable settings and lists are managed by JobyX; you don't need to do anything here to save those things.
If your sub returns (0)
, Joby will remove all commands, lists, variables, and avariables specific to your module from memory. This allows you to customize a bot on-the-fly. Note that the actual module code will not be removed, but all connections between JobyX and it will be effectively severed. No hooks will be called.
The core provides some other subs that you can call from your modules:
@items
in columns. The width will never exceed 79. The return list is suitable for feeding to &::qtell.
$handle
is executing command $command
with arguments $args
. Note that $args
is not a list! Doing this:
&::execcommand('Hitmonchan','addlist','censor','tupac');
would not have the desired effect. Hitmonchan would be told how to use AddList. This is probably what you want:
&::execcommand('Hitmonchan','addlist','censor tupac');
$avariable
avariable.
$handle
's $variable
variable. Note that this is not always the same as the data contained in the %var
hash in the user's parray, which will be unset if the variable hasn't been set by the user.
push()
ed to an array and shift()
ed off by the input loop after your handler returns. This means that any lines read via &::getnextline
will be parsed by the input loop, which is probably a good thing. Also note that \&idle
subs may be called while waiting for an input line, so don't do anything in your idle sub that could interfere with the sub you're calling &::getnextline
from.
$handle
. Use &::userinfo
if all the info you need is supplied by it, as it is more efficient. Generally, you should only need to call &::getpi
for $flags
or \%ratings
. Note that $::last_flags
could possibly take care of the flags you need, but on some servers xtells don't contain flags, so look out.
If $expect_complete
is on, &::getpi
will not return success when the server returns a completed handle. For example, if the user HotPawn existed, &::getpi("hotpaw",1)
would fail. If $expect_complete
was off, or omitted, &::getpi
would return stats for ``HotPawn''. (These examples assume that the user ``HotPaw'' doesn't exist.)
return (
&::userinfo
will be more efficient.
$ratings->{'blitz'}
will return the user's blitz rating, for example.
$email
, admin status is required.
);
$item
is in the $list
list.
$item
is in ${owner}
's personal $list
list.
@args
to module $module
via the ICS_ipc
hook. The return value will be whatever was returned from the other module's ICS_ipc
. If module $module
is not loaded, or does not implement ICS_ipc
, undef
will be returned. Since modules are meant to be independant, there is little use for this. Flexibility is the key though, so I leave the judgement up to you.
$module
is currently loaded.
$string
as if it was received from the server. Be careful not to start an infinite loop by making it parse a tell to the same command handler that you're calling &::parse
from.
@lines
to user $handle
. Don't worry about newlines embedded in the lines; they will be handled the same was as if the strings on both sides were separate elements of @lines
. The user's Height variable is taken into account here.
$handle
meets the qualifications specified in $userlevel
. See Qualifications at the beginning of this document for information on qualification strings.
$padding
is on, each cell will be wrapped with spaces to make the output easier to read. Please note that cells will not wrap, even if the width of the table would exceed 79! Cells are left-aligned by default. To make a cell right-aligned, prefix the content with character 0. The format of @rows
is:
@rows=(\@row1, \@row2, ... \@rowN)
For each row, you can do either of the following:
# Standard row. @row1=('Column 1','Column 2', ... 'Column X'); # Second column, value "50", is right-aligned. Note that just "\050" # won't work... character 50 will be displayed left-aligned. See the # perlop manpage. @row2=('Blitz:',"\00050"); # Divider. @row2=();
The number of columns actually displayed depends on the number of items in the longest row.
For example, the following code:
&::table( ['Manager','Level','Status'], [], ['ChrisHowie','Operator(99)','Always busy'], ['Venom','Master(30)'], );
Would return a list that, when used with &::qtell, would display the following:
+-----------------------------------------+ | Manager | Level | Status | |------------|--------------|-------------| | ChrisHowie | Operator(99) | Always busy | | Venom | Master(30) | | +-----------------------------------------+
Nifty, no? Of course, you can modify the returned list before you pass it to &::qtell, either to add a few status rows to the end, or something completely bizzare.
If you want to create a merged cell, span the area with undef
s. For example:
&::table( ['Statistics',undef], [], ['One','Two'], ['Three',undef], );
Would result in this:
+--------------+ | Statistics | |--------------| | One | Two | | Three | | | Four | +--------------+
Note that you cannot supply undef
as the content for the first column, or your command handler will die.
To right-align a cell, preceed the cell with the null character. For example, ``\0001.00'' will display ``1.00'' aligned to the right.
There is no special way to create extra-tall rows. If you want to wrap cells, you'll have to do something really interesting and complicated.
return (
);
As for the format of the player array:
[
&::getvar
is more reliable, because it will take into account unset variables with default values.
]
Though not hooks in the traditional sense, JobyX will let your module know when something interesting happens on the server, or locally. It is not neccessary to supply any of these. (Exceptions: ICS_init
and ICS_term
.) If your module doesn't have a specific hook, JobyX will skip it when a hook event occurs. Modules receive hooks in the order that they were loaded in.
The list is sorted somewhat alphabetically; similar hooks are grouped.
ICS_init($username,$version,$handle)
$handle
is trying to insert your module. See Loading a Module above.
ICS_connect($handle,$guest,$ip)
$handle
has connected to the server. $guest
and $ip
will only be available if JobyX's server account has admin status.
ICS_disconnect($handle)
$handle
has left the server.
ICS_dis_connect($handle,$arrive)
$handle
has connected if $arrive
, else disconnected.
ICS_finger($handle_of_caller,$requested_handle)
$handle_of_caller
is requesting finger information on $requested_handle
. You can return a string to append to the finger notes, with newlines if more than one line is required. In my Tourney module, I return the user's tournament statistics. Each module's return information will be separated with a newline automatically.
ICS_input($line)
$line
will not have a trailing newline, and any prompt will be removed. A return value of (1)
tells JobyX not to parse this line further. A return value of (2)
tells JobyX to not parse this line further, and also to not call ICS_input
for any remaining modules. Generally, if you want to stop JobyX from parsing, (1)
should be used. There's no telling what another module can see in this line. An exception is when your module is going to take exclusive action on this line and you don't want another module to take action. For example, interpreting a shout as a command and you don't want another module doing the same thing and flooding the shouter.
ICS_tell($handle,$flags,$message)
$handle
(with flags $flags
) has told JobyX $message
. A return value of (1)
will make JobyX ignore this command. A return value of (2)
will make JobyX ignore this command, and also not call ICS_tell
for any remaining modules. Generally, you should use (2)
, for obvious reasons. We don't want a whole load of modules interpreting a tell as a command and responding at once. For example, a VoteBot module could do something like:
sub ICS_tell { my($handle,undef,$mess)=@_; if ($mess=~/^ \d+ [a-z]? $/xi) { my @res=&::userinfo($handle); $res[0] or return; my $lt=$res[1]->[6]->{'lasttopic'}; ($lt>0) or return; &::execcommand($_[0],'vote',"$lt $message"); return 2; } }
to make it behave like giantfish's VoteBot. (Note that the Vote command would most likely be setting the user's $misc->{'lasttopic'}
miscellaneous setting.
ICS_ipc($module,@args)
$module
wants to talk to your module. This can be used to share any type of data, which is transmitted in @args
. See &::ipc
.
@list
of lines. You can display anything that you think is relevant to the current state of the module. For example, a Tourney module might display how many tourneys are in memory. This can be very useful in debugging.
ICS_lsmod($maxlen)
ICS_module_info
, but the return should be one string, without newlines. Called when lsmod is executed. It is perfectly okay to return nothing. Should yo decide to return anything, it should be a brief quip about the internal state of the module. Try not to exceed $maxlen
characters. $maxlen
is found by subtracting the length of your module name and number from 79, thus giving the longest string that you can return without wrapping. You can exceed this if neccessary, but you should try to keep long output for ICS_module_info
.
ICS_pre_module_load($module)
$module
is about to be loaded. A return value of (1,$error)
will cancel the load and supply the error message Module1 refused the load of Module2. $error
. Note that at this point, the module has not been loaded yet. If you need to do &::ipc
with another module, use the ICS_module_loaded
hook instead.
ICS_module_loaded($module)
$module
has been loaded.
ICS_pre_module_unload($module)
$module
is about to be unloaded. A return value of (1,$error)
will cancel the unload and supply the error message Module1 refused the removal of Module2. $error
. This can be useful if your module depends on another. Note that at this point, the module has not been unloaded yet. &::ipc
will still work.
ICS_module_unloaded($module)
$module
has been unloaded.
Occasionally, two modules may want the same command, list, variable, or avariable. How does JobyX determine which module's command handler is called? Simple: The first module that was loaded is given preference.
Note that both commands are stored in memory, so if the first module is removed, the second module's command will become available. There is no way to specifically call the second module's handler while the first module is loaded.
Note that this applies to lists, variables, and avariables as well.
If you know of other modules that have the same commands/lists/etc as one of yours, you can do something like the following:
package JX_Mod::Foo; sub ICS_init { if (&::ismoduleloaded('Bar')) { return (0,"Bar conflicts with this module."); } # return commands and stuff... } sub ICS_pre_module_load { if (shift eq 'Bar') { return (1,"Bar conflicts with this module."); } }
While not very compatible, this method is the best when you can't help a conflict. The downside is that you must be aware of the module that is causing the conflict. The upside is that the other module's author need not insert code to check for your module, becuase the ICS_pre_module_load
hook will take care of it.
Try to name your commands and stuff with names that apply to your module. For example, my Vote module names the command used to create topics CreateTopic. Naming it Create could cause conflicts with other modules very easily.
To end on a happy note, if JobyX detects a conflict when you are using insmod, it will include them in the response. The best thing to do at that point is to use rmmod and try to fix the conflict.
JobyX provides a fully-functional helpfile system. By using the Help command, users can request documentation on any command or topic. To create help files for your module, simply place them in path_to_jobyx/help/module_name
. For example, my Tourney module places its helpfiles in /home/chris/jobyx/help/Tourney/.
Usage files can be placed in path_to_jobyx/usage/module_name
.
Conflicting helpfiles are dealt with in the same manner as commands/lists/etc. The first module loaded is given preference.
When the Help command is called, JobyX builds an index of all of the helpfiles, working through each module's help folders in the order that they were loaded. If two files have the same name, the second is ignored. The practical upshot of this work is for helpfile name completion. For example, if a user types help me, and the first module has the ``mean'' file, while the second has the ``metaphor'' helpfile, the result will be a Helpfile ambiguous error. If JobyX looked in each directory until it found a match, ``mean'' would be displayed.
To restrict access to helpfiles and usage files, the following check is performed: When a user requests a help/usage file, the entire command database is checked for commands with the same
Aside from plain helpfiles, you can use some directives to mutate them based on a user's qualifications. Directives are in effect until the _next_ directive is encountered. Note that directives are not effective in usage files. If a directive starts with ``#'', it must be flush left; if ``%'', then it can appear anywhere.
$string
.
To save time, the index builder only reads the first line of each helpfile. As a result, this directive is effective only on the first line.
#SEE
directive, this line will limit visibility of the helpfile. Like #SEE
, it is ignored by the parser. When the indexer encounters this line, it will search for a command with the same name within the same module. If it finds a match, the visibility level of the helpfile will be the same as the visibility of the matching command. This will save you time when editing command levels; you won't have to edit each helpfile as well. Note that this will cause a performance hit when building the index, but that's why we don't build the index each time a helpfile is requested.
Because it is also effective only on the first line, the #SEE
and #COMMAND
directives are mutually exclusive.
NOTE: Since usage files are always associated with commands, they have, hypothetically speaking, an implied #COMMAND
directive in them. That is, the usage command will filter visibility based on the visibility of the matching command.
$string
to read the next chunk.
$string
to read the next chunk.
JX_Mod::CORE
's namespace. You cannot extend expressions across lines.
Single quotes were chosen because you can always use double quotes in place of single quotes, but the reciprocal isn't always true.
Here is an example helpfile for the command CreateTourney from my Tourney module:
#COMMAND Creates a tournament. #USAGE
Quite simple, really. I could also have done #SEE 20
instead of using #COMMAND
, but now this helpfile and the CreateTourney command are effectively linked; any change to the visibility of CreateTourney will be followed by the helpfile.
Here is a more complex example of an index helpfile:
Hello! My name is %me, and I am your friendly tournament robot! For a list of tournaments, type "%tell lt". To join a tournament, type "%tell join <number>". #QUALIFIED 20 For tournament managing directions, type "%tell help tm_guide". #QUALIFIED ly:admin For server admin commands, type "%tell help admin_guide". #BREAK Have fun!
Notice the creative use of directives to provide all staff members, TM or admin, with directions, and only when they are a TM or admin.
Please note that #comments are not allowed, and will be displayed. The # directive notation is used because it is fairly common in programming languages.
As far as restricting usage files, JobyX will try to find a command with the same name as a usage file requested. If it finds one, the user must be qualified to see the command in order to view the file. If JobyX can't find a command with the same name, the usage file will be displayed regardless of the user's level. (NOTE: In the future, helpfiles may be dealt with in the same manner; I.E. finding a matching command.)
While I'm not aware of any yet, this is the first release, so I'm sure there are some. Post any you find on the Bug Tracker on the SourceForge project page at http://www.sourceforge.net/projects/jobyx and I'll fix them ASAP.
Special thanks to the Free Internet Chess Server (FICS - http://www.freechess.org) and the DeepNet Chess Server (DNCS - http://chess.deepnet.com) for allowing me to test JobyX on their servers.