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 )