From 0d7f0bf18440b1d51b82d0099936428c25349b9a Mon Sep 17 00:00:00 2001 From: Pragmatic Software Date: Sun, 6 Sep 2015 22:17:07 -0700 Subject: [PATCH] Add loadable core plugins These are different from the loadable factoid modules. The factoid modules are external executable shell commands that take stdin as arguments and print to stdout as a return value. As such, they are not integrated into the bot and cannot make use of the bot's internal subroutines. These plugins are loaded internally and integrated into the bot such that they can interface with the bot's internal subroutines and state. All files in the Pluggable directory 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. --- PBot/EventDispatcher.pm | 32 +++++++- PBot/PBot.pm | 3 + PBot/Pluggable.pm | 163 +++++++++++++++++++++++++++++++++++++ PBot/Pluggable/_Example.pm | 42 ++++++++++ PBot/Pluggable/_Readme | 8 ++ 5 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 PBot/Pluggable.pm create mode 100644 PBot/Pluggable/_Example.pm create mode 100644 PBot/Pluggable/_Readme 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.