diff --git a/PBot/EventDispatcher.pm b/PBot/EventDispatcher.pm index d628b0d4..76736ffc 100644 --- a/PBot/EventDispatcher.pm +++ b/PBot/EventDispatcher.pm @@ -31,14 +31,38 @@ sub dispatch_event { my $ret = undef; if (exists $self->{handlers}->{$event_type}) { - foreach my $handler (@{$self->{handlers}->{$event_type}}) { - $ret = $handler->($event_type, $event_data); + for (my $i = 0; $i < @{$self->{handlers}->{$event_type}}; $i++) { + my $handler = @{$self->{handlers}->{$event_type}}[$i]; + + eval { + $ret = $handler->($event_type, $event_data); + }; + + if ($@) { + chomp $@; + $self->{pbot}->{logger}->log("Error in event handler: $@\n"); + $self->{pbot}->{logger}->log("Removing handler.\n"); + splice @{$self->{handlers}->{$event_type}}, $i--, 1; + } + return $ret if $ret; } } - foreach my $handler (@{$self->{handlers}->{any}}) { - $ret = $handler->($event_type, $event_data); + for (my $i = 0; $i < @{$self->{handlers}->{any}}; $i++) { + my $handler = @{$self->{handlers}->{any}}[$i]; + + eval { + $ret = $handler->($event_type, $event_data); + }; + + if ($@) { + chomp $@; + $self->{pbot}->{logger}->log("Error in event handler: $@\n"); + $self->{pbot}->{logger}->log("Removing handler.\n"); + splice @{$self->{handlers}->{any}}, $i--, 1; + } + return $ret if $ret; } diff --git a/PBot/PBot.pm b/PBot/PBot.pm index b74c7e62..da33e52c 100644 --- a/PBot/PBot.pm +++ b/PBot/PBot.pm @@ -50,6 +50,7 @@ use PBot::Timer; use PBot::AntiAway; use PBot::AntiKickAutoRejoin; use PBot::Refresher; +use PBot::Pluggable; sub new { if(ref($_[1]) eq 'HASH') { @@ -144,6 +145,8 @@ sub initialize { %conf ); + $self->{pluggable} = PBot::Pluggable->new(pbot => $self, %conf); + # load registry entries from file to overwrite defaults $self->{registry}->load; diff --git a/PBot/Pluggable.pm b/PBot/Pluggable.pm new file mode 100644 index 00000000..fa79e6d2 --- /dev/null +++ b/PBot/Pluggable.pm @@ -0,0 +1,163 @@ +# File: Pluggable.pm +# Author: pragma- +# +# Purpose: Loads and manages pluggable modules. + +package PBot::Pluggable; + +use warnings; +use strict; + +use File::Basename; +use Carp (); + +sub new { + if(ref($_[1]) eq 'HASH') { + Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference"); + } + + my ($class, %conf) = @_; + + my $self = bless {}, $class; + $self->initialize(%conf); + return $self; +} + +sub initialize { + my ($self, %conf) = @_; + + $self->{pbot} = delete $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); + + $self->{modules} = {}; + + $self->{pbot}->{commands}->register(sub { $self->load_cmd(@_) }, "plug", 90); + $self->{pbot}->{commands}->register(sub { $self->unload_cmd(@_) }, "unplug", 90); + $self->{pbot}->{commands}->register(sub { $self->list_cmd(@_) }, "pluglist", 0); + + $self->autoload(); +} + +sub autoload { + my $self = shift; + + $self->{pbot}->{logger}->log("Loading pluggable modules ...\n"); + my $module_count = 0; + + my @modules = glob 'PBot/Pluggable/*.pm'; + + foreach my $module (sort @modules) { + $module = basename $module; + $module =~ s/.pm$//; + + # do not load modules that begin with an underscore + next if $module =~ m/^_/; + + $module_count++ if $self->load($module) + } + + $self->{pbot}->{logger}->log("$module_count module" . ($module_count == 1 ? '' : 's') . " loaded.\n"); +} + +sub load { + my ($self, $module) = @_; + + $self->unload($module); + + my $class = "PBot::Pluggable::$module"; + + $self->{pbot}->{refresher}->{refresher}->refresh_module("PBot/Pluggable/$module.pm"); + + my $ret = eval { + eval "require $class"; + + if ($@) { + chomp $@; + $self->{pbot}->{logger}->log("Error loading $module: $@\n"); + return 0; + } + + $self->{pbot}->{logger}->log("Loading $module\n"); + my $mod = $class->new(pbot => $self->{pbot}); + $self->{modules}->{$module} = $mod; + $self->{pbot}->{refresher}->{refresher}->update_cache("PBot/Pluggable/$module.pm"); + return 1; + }; + + if ($@) { + chomp $@; + $self->{pbot}->{logger}->log("Error loading $module: $@\n"); + return 0; + } + + return $ret; +} + +sub unload { + my ($self, $module) = @_; + + $self->{pbot}->{refresher}->{refresher}->unload_module("PBot::Pluggable::$module"); + $self->{pbot}->{refresher}->{refresher}->unload_subs("PBot/Pluggable/$module.pm"); + + if (exists $self->{modules}->{$module}) { + eval { + $self->{modules}->{$module}->unload; + delete $self->{modules}->{$module}; + }; + if ($@) { + chomp $@; + $self->{pbot}->{logger}->log("Warning: got error unloading module $module: $@\n"); + } + $self->{pbot}->{logger}->log("Pluggable module $module unloaded.\n"); + return 1; + } else { + return 0; + } +} + +sub load_cmd { + my ($self, $from, $nick, $user, $host, $arguments) = @_; + + if (not length $arguments) { + return "Usage: plug "; + } + + if ($self->load($arguments)) { + return "Loaded $arguments plugin."; + } else { + return "Plugin $arguments not found."; + } +} + +sub unload_cmd { + my ($self, $from, $nick, $user, $host, $arguments) = @_; + + if (not length $arguments) { + return "Usage: unplug "; + } + + if ($self->unload($arguments)) { + return "Unloaded $arguments plugin."; + } else { + return "Plugin $arguments not found."; + } +} + +sub list_cmd { + my ($self, $from, $nick, $user, $host, $arguments) = @_; + + my $result = "Loaded plugins: "; + my $count = 0; + my $comma = ''; + + foreach my $plugin (sort keys $self->{modules}) { + $result .= $comma . $plugin; + $count++; + $comma = ', '; + } + + $result .= 'none' if $count == 0; + + return $result; +} + +1; diff --git a/PBot/Pluggable/_Example.pm b/PBot/Pluggable/_Example.pm new file mode 100644 index 00000000..234abdd9 --- /dev/null +++ b/PBot/Pluggable/_Example.pm @@ -0,0 +1,42 @@ + +package PBot::Pluggable::_Example; + +use warnings; +use strict; + +use Carp (); + +sub new { + Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref $_[1] eq 'HASH'; + my ($class, %conf) = @_; + my $self = bless {}, $class; + $self->initialize(%conf); + return $self; +} + +sub initialize { + my ($self, %conf) = @_; + + $self->{pbot} = delete $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); + + $self->{pbot}->{event_dispatcher}->register_handler('irc.public', sub { $self->on_public(@_) }); +} + +sub unload { + my $self = shift; + # perform plugin clean-up here + # normally we'd unregister the 'irc.public' event handler; however, the + # event dispatcher will do this automatically for us when it sees there + # is no longer an existing sub. +} + +sub on_public { + my ($self, $event_type, $event) = @_; + my ($nick, $user, $host, $msg) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->args); + + $self->{pbot}->{logger}->log("_Example plugin: got message from $nick!$user\@$host: $msg\n"); + + return 0; +} + +1; diff --git a/PBot/Pluggable/_Readme b/PBot/Pluggable/_Readme new file mode 100644 index 00000000..857ddefe --- /dev/null +++ b/PBot/Pluggable/_Readme @@ -0,0 +1,8 @@ +Loadable plugins live here. + +All files not beginning with an underscore will be automatically loaded +at bot start-up. + +Plugins (including those starting with an underscore) can be manually loaded +or unloaded with the `plug` and `unplug` commands. Use `pluglist` to list +loaded plugins.