Bot-Cobalt
view release on metacpan or search on metacpan
lib/Bot/Cobalt/Manual/Plugins.pod view on Meta::CPAN
=head3 Auth-related methods
The Core provides access to a L<Bot::Cobalt::Core::ContextMeta::Auth>
object; methods can be called to determine user authorization levels.
These are the most commonly used methods; see
L<Bot::Cobalt::Core::ContextMeta> and
L<Bot::Cobalt::Core::ContextMeta::Auth> for more.
=head4 level
Retrieves the user's authorized level (or '0' for unauthorized users).
Requires a context and a nickname:
## inside a msg or command handler, f.ex:
my ($self, $core) = splice @_, 0, 2;
my $msg = ${ $_[0] };
my $context = $msg->context;
my $nick = $msg->src_nick;
my $level = $core->auth->level($context, $nick);
Auth levels are fairly flexible; it is generally a good idea for your
plugin to provide some method of configuring required access levels,
either via a configuration file or a B<Opts> directive in
C<plugins.conf>.
=head4 username
Retrieves the "username" for an authorized user (or empty list if the user
is not currently authorized).
Requires a context and a nickname, similar to L</level>:
my $username = core()->auth->username($context, $nick);
unless ($username) {
## this user isn't authorized
}
=head3 Logging
The Cobalt core provides a B<log> method that writes to the LogFile
specified in cobalt.conf (and possibly STDOUT, if running with --nodetach).
This is actually a L<Bot::Cobalt::Logger> instance, so all methods found
there
apply. Typically, plugins should log to B<info>, B<warn>, or B<debug>:
core()->log->info("An informational message");
core()->log->warn("Some error occured");
core()->log->debug("some verbose debug output for --debug");
A plugin should at least log to B<info> when it is registered or
unregistered; that is to say, inside B<Cobalt_register> and
B<Cobalt_unregister> handlers.
=head3 Timers
Core timers live in B<< core()->TimerPool >>; if need be, you can
access the timer pool directly. It is a hash keyed on timer ID.
Timer methods are provided by the L<Bot::Cobalt::Core::Role::Timers> role.
Each individual timer is a L<Bot::Cobalt::Timer> object; if you plan to
manipulate a created timer, you'll likely want to consult that POD.
Typically most plugins will only need the following functionality; this
only covers the hash-based interface to
L<Bot::Cobalt::Core::Role::Timers/timer_set>, so review the aforementioned
documentation if you'd like to use the object interface instead.
=head4 timer_set
Set up a new timer for an event or message.
## Object interface:
core()->timer_set( $timer_object );
## Hash interface:
core()->timer_set( $delay, $ev_hash );
core()->timer_set( $delay, $ev_hash, $id );
Returns the timer ID on success, boolean false on failure.
Expects at least a delay (in seconds) and a hashref specifying what to
do when the delay has elapsed.
## New 60 second 'msg' timer with a random unique ID:
## Send $string to $channel on $context
## (A triggered 'msg' timer broadcasts a 'message' event)
my $id = $core->timer_set( 60,
{
## The type of timer; 'msg', 'action' or 'event':
Type => 'msg',
## This is a 'msg' timer; we need to know what to send
## 'action' carries the same syntax
Context => $context,
Target => $channel,
Text => $string,
}
);
Here's the same timer, but using the pure object syntax:
use Bot::Cobalt::Timer;
$core->timer_set(
Bot::Cobalt::Timer->new(
core => $core,
context => $context,
target => $channel,
text => $string,
type => 'msg',
delay => 60
);
);
If no B<Type> is specified, I<event> is assumed:
## Trigger event $event in $secs with (optional) @args:
my $id = $core->timer_set( $secs,
{
Event => $event,
Args => [ @args ],
}
);
## ... same thing, but object interface:
my $id = $core->timer_set(
Bot::Cobalt::Timer->new(
core => $core,
event => $event,
args => \@args,
);
);
You can tags packages with your plugin's B<Alias>, if you'd like;
if an Alias is set, you'll be able to clear all timers by alias via
L</timer_del_alias> or retrieve them via L</timer_get_alias>:
## Alias-tagged timer
my $id = $core->timer_set( $secs,
{
Event => $event,
Args => [ @args ],
## Safely retrieve our $self object's plugin alias:
Alias => $core->get_plugin_alias( $self ),
},
);
(The L<Bot::Cobalt::Timer> object interface uses the B<alias> attribute.)
Additionally, L<Bot::Cobalt::Plugin::PluginMgr> automatically tries to clear
plugin timers for unloaded plugins; this only works for Alias-tagged timers.
Without a specified Alias, a timer is essentially considered ownerless --
it will happily fire at their scheduled time even if the issuing plugin
is gone.
By default, a random timer ID is chosen (and returned).
You can also specify an ID:
## Set a timer with specified ID 'MyTimer'
## Will overwrite any preexisting timers with the same ID
$core->timer_set(
$secs,
{ Event => $event, Args => [ @args ] },
'MyTimer'
);
(The L<Bot::Cobalt::Timer> object interface uses the B<id> attribute.)
This can be used for resetting timers you've already set; grab the ID
returned by a C<timer_set()> call and reset it to change the event or delay.
You may want C<timestr_to_secs> from L<Bot::Cobalt::Utils> for easy
conversion of human-readable strings into seconds. This is, of course,
included by default if you C<< use L<Bot::Cobalt::Common> >>.
If you need better accuracy, you'll need to use your own alarm()/delay()
calls to L<POE::Kernel>; the timer pool is checked every second or so.
Arguments specified in the B<Args> array reference or B<args> object
attribute will be relayed to plugin event handlers just like any other
event's parameters:
sub Bot_some_timed_event {
## Called by a timer_set() timer
my ($self, $core) = splice @_, 0, 2;
my $firstarg = ${ $_[0] };
my $second = ${ $_[1] };
}
=head4 timer_del
Delete a timer by ID.
my $deleted = $core->timer_del( $id );
Returns the deleted timer object, or nothing if there was no such
ID.
The returned result (if there is one) can be fed back to L</timer_set>
if needed; it will be a L<Bot::Cobalt::Timer> object:
## hang on to this timer for now:
my $postponed = $core->timer_del( $id ) ;
## . . . situation changes . . .
$postponed->delay(60);
if ( $core->timer_set( $postponed ) ) {
## readding postponed timer successful
}
=head4 timer_del_alias
Delete all timers owned by the specified alias:
my $plugin_alias = $core->plugin_get_alias( $self );
my $deleted_count = $core->timer_del_alias( $plugin_alias );
Only works for timers tagged with their Alias; see L</timer_set>.
Timers with no Alias tag are considered essentially "ownerless" and left
to their own devices; they'll fail quietly if the timed event was handled
by an unloaded plugin.
This is also called automatically by the core plugin manager
(L<Bot::Cobalt::Plugin::PluginMgr>) when a plugin is unloaded.
=head4 timer_get_alias
Find out which active timerIDs are owned by the specified alias:
my $plugin_alias = $core->plugin_get_alias( $self );
my @active_timers = $core->timer_get_alias( $plugin_alias );
=head4 timer_get
Retrieve the L<Bot::Cobalt::Timer> for this active timer (or undef if not
found).
my $this_timer = $core->timer_get($id);
=head2 Syndicated core events
These are events sent by L<Bot::Cobalt::Core> when various core states
change.
You should probably return PLUGIN_EAT_NONE on all of these, unless
you are absolutely sure of what you are doing.
=head3 Plugin related events
=head4 Bot_plugins_initialized
Broadcast when the initial plugin load has completed at start-time.
Carries no arguments.
=head4 Bot_plugin_error
Broadcast when the syndicator reports an error from a plugin.
The only argument is the error string reported by
L<POE::Component::Syndicator>.
These messages are also logged to 'warn' by default.
=head3 Timer related events
=head4 Bot_executed_timer
Broadcast whenever a timer ID has been executed.
The only argument is the timer ID.
=head4 Bot_deleted_timer
Broadcast whenever a timer ID has been deleted.
The first argument is the timer ID.
The second argument is the removed L<Bot::Cobalt::Timer> object.
=head3 Ignore related events
=head4 flood_ignore_added
Broadcast by L<Bot::Cobalt::IRC> when a temporary anti-flood ignore has
been placed.
Arguments are the server context name and the mask that was added,
respectively.
=head4 flood_ignore_deleted
Broadcast by L<Bot::Cobalt::IRC> when a temporary anti-flood ignore has
expired and been removed.
Arguments are the same as L</flood_ignore_added>.
=head1 PLUGIN DESIGN TIPS
=head2 Useful tools
=head3 Bot::Cobalt
Importing L<Bot::Cobalt> via 'use Bot::Cobalt' brings in the
L<Bot::Cobalt::Core::Sugar> functions.
These provide simple syntax sugar for accessing the L<Bot::Cobalt::Core>
singleton and common methods such as B<send_event>; consult the
L<Bot::Cobalt::Core::Sugar> documentation for details.
=head3 Bot::Cobalt::Common
L<Bot::Cobalt::Common> is a simple exporter that will pull in common constants
and utilities from L<Object::Pluggable::Constants>, L<IRC::Utils>, and
L<Bot::Cobalt::Utils>.
Additionally, C<use Bot::Cobalt::Constant> will enable the B<strict> and
B<warnings> pragmas.
This is provided as a convenience for plugin authors; rather than importing
from a goodly handful of modules, you can simply:
use Bot::Cobalt::Common;
Declaring strict and warnings explicitly are still good practice.
See L<Bot::Cobalt::Common> for details.
=head3 Bot::Cobalt::DB
L<Bot::Cobalt::DB> provides an easy object-oriented interface to storing
and retrieving Perl data structures to/from BerkeleyDB via L<DB_File>.
Useful when a plugin has some persistent data it needs to
keep track of, but storing it in memory and serializing to/from disk is
too expensive.
lib/Bot/Cobalt/Manual/Plugins.pod view on Meta::CPAN
Instead, you can break the loop into event handlers and yield back to
the event loop, cooperatively multitasking with other events.
The below example processes a large list of items, pushing remaining
items back to the 'worker' event handler after iterating 100 items.
sub Cobalt_register {
## ... initialization...
## ... register for myplugin_start_work, myplugin_do_work
}
## Some event that starts a long-running loop:
sub Bot_myplugin_start_work {
my ($self, $core) = splice @_, 0, 2;
my @items = long_list_of_items();
## begin _do_work
## pass our @items to it, for example:
$core->send_event( 'myplugin_do_work', [ @items ] );
return PLUGIN_EAT_ALL
}
sub Bot_myplugin_do_work {
my ($self, $core) = splice @_, 0, 2;
## our remaining items:
my $itemref = ${ $_[0] };
my @items = @$itemref;
## maximum number of elements to process before yield:
my $max_this_run = 100;
while (@items && --$max_this_run != 0) {
my $item = shift @items;
## ... do some work on $item ...
}
## if there's any items left, push them and yield:
if (@items) {
$core->send_event( 'myplugin_do_work', [ @items ] );
} else {
## no items left, we are finished
## tell pipeline we're done, perhaps:
$core->send_event( 'myplugin_finished_work' );
}
return PLUGIN_EAT_ALL
}
For more fine-grained control, consider running your own POE::Session;
see L</"Spawning your own POE::Session">, below.
=head2 Spawning your own POE::Session
There's nothing preventing you from spawning your own L<POE::Session>;
your session will run within Cobalt's L<POE::Kernel> instance and POE
event handlers will work as-normal.
Motivations for doing so include fine-grained timer control, integration
with POE bits such as the POE::Component and POE::Wheel namespaces
. . . and the fact that POE is pretty great ;-)
It's worth noting that many POE Components use B<get_active_session> to
determine where to send responses. It may sometimes be necessary to use
intermediary "proxy" methods to ensure a proper destination session is
set in the POE::Component in use.
See L<Bot::Cobalt::Plugin::WWW> source for an example of a plugin that uses
its own POE::Session and does this (when issuing HTTP requests to
L<POE::Component::Client::HTTP>).
=head2 Manipulating plugin pipeline order
L<Object::Pluggable> allows you to manipulate the plugin pipeline order;
that is to say, the order in which events will hit plugins.
For example, when writing a plugin such as an input filter, it can be useful to move
your plugin towards the top of the plugin pipeline:
## With 'use Bot::Cobalt':
core->pipeline->bump_up( plugin_alias($self) );
See L<Object::Pluggable::Pipeline> for details.
Plugin managers are not required to take any special consideration of a
plugin's previous position in the case of a plugin (re)load.
=head1 IRC CAVEATS
=head2 IRC casemapping rules
Determining whether or not nicknames and channels are equivalent on IRC
is not as easy as it looks.
Per the RFC (L<http://tools.ietf.org/html/rfc1459#section-2.2>):
the characters {}| are
considered to be the lower case equivalents of the characters []\,
respectively
This set ( {}| == []\ ) is called B<strict-rfc1459> and identified as such in
a server's I<ISUPPORT CASEMAPPING=> directive.
More often, servers use the set commonly identified as B<rfc1459>:
## rfc1459 lower->upper case change: {}|^ == []\~
$value =~ tr/a-z{}|^/A-Z[]\\~/;
Some servers may use normal ASCII case rules; they will typically announce
B<ascii> in I<CASEMAPPING=>.
L<Bot::Cobalt::IRC> will attempt to determine and save a server's CASEMAPPING value
at connect time. Some broken server configurations announce junk in
I<CASEMAPPING> and their actual valid casemapping ruleset in I<CHARSET>;
L<Bot::Cobalt::IRC> will fall back to I<CHARSET> if I<CHARSET> is a valid casemap
but I<CASEMAPPING> is invalid. If all else fails, B<rfc1459> is used.
The saved value can be used to feed C<eq_irc> and friends from L<IRC::Utils> and
determine nickname/channel equivalency.
( run in 1.389 second using v1.01-cache-2.11-cpan-437f7b0c052 )