1
0
Fork 0
mirror of https://github.com/perlbot/perlbuut synced 2025-06-07 10:35:41 -04:00

Merge branch 'master' of github.com:perlbot/perlbuut

This commit is contained in:
Ryan Voots 2018-05-22 03:33:34 -04:00
commit 2eba1ab8b0
101 changed files with 4 additions and 8534 deletions

View file

@ -22,4 +22,8 @@ There is a docs/ directory but it's woefully outdated and doesn't reflect a numb
Good question - what is the license?
This is a semi-complicated situation as while the code is obstensibly open source there have been several authors involved. Notably myself (simcop2387), buu, and b_jonas. Buu started the original project (see https://github.com/simcop2387/buutbot ) but then disappeared not long after i forked it (with permission) to make a new version of perlbot. b_jonas was involved in the creation of some of a few plugins, if not more, that made the framework incredibly flexible.
buu has agreed to put his code under the GPL version 3 ( L<https://en.wikipedia.org/wiki/GNU_General_Public_License#Version_3> ) or at your option any later version.
Shlomi Fish licences his changes under any and all of the Expat license, the
CC0, the same terms as perl 5, and the Artistic 2.0 license.
=cut

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,198 +0,0 @@
10
dir
475
svn://erxz.com/bb3/branches/perlbuut/bin
svn://erxz.com/bb3
2009-10-03T22:53:42.528878Z
475
simcop
has-props
dcb1cea6-7f7e-4c78-8a22-148ace8ce36e
console
file
2009-10-03T22:54:20.000000Z
35d0f0f3f22d1c5c9269a11438e8b9bf
2009-10-03T22:53:42.528878Z
475
simcop
has-props
650
cpan_fetch.pl
file
2009-10-03T22:54:20.000000Z
a060d683a14b4caf9b955835d84928a2
2009-10-03T22:53:42.528878Z
475
simcop
190
bb3
file
2009-10-03T22:54:20.000000Z
19a8492fe2e1f4555b3e0420804bc08e
2009-10-03T22:53:42.528878Z
475
simcop
has-props
1416
evalserver_test.pl
file
2009-10-03T22:54:20.000000Z
6b3a1a206e0f305decc280b74a0a83f3
2009-10-03T22:53:42.528878Z
475
simcop
454
evalserver
file
2009-10-03T22:54:20.000000Z
a89e0e8f5969ec905d7641c00a8be339
2009-10-03T22:53:42.528878Z
475
simcop
has-props
540

View file

@ -1,5 +0,0 @@
K 14
svn:executable
V 1
*
END

View file

@ -1,5 +0,0 @@
K 14
svn:executable
V 1
*
END

View file

@ -1,5 +0,0 @@
K 14
svn:executable
V 1
*
END

View file

@ -1,62 +0,0 @@
#!/usr/bin/perl
use POSIX qw/setsid/;
use Getopt::Std;
my %OPTS;
# Localize the @ARGV so we preserve it
# since getopts destructively modifies it
# we need it to be intact so we can restart
# ourselves later.
BEGIN { local @ARGV=@ARGV; getopts("dm:M:c:p:", \%OPTS) };
# d daemon
# m only this role(s)
# M Every role but this Not Implemented
# c conf file
# p plugin conf file
# Guess we're being activated inside bin/, so go up a directory.
BEGIN {
if( not -e 'lib' and not -e 'etc' and -e 'bb3' ) {
chdir "..";
}
elsif( my @par_dirs = grep /tmp.*par.*cache-/, @INC ) {
# We're running under PAR!
chdir( ( grep /inc$/, @par_dirs ) [0] ); #Find the one that ends in inc/
mkdir "var"; # We need one of these..
}
elsif( $0 =~ '/' and $0 ne 'bin/bb3' ) {
my $path = $0;
$path =~ s{bin/bb3$}{};
chdir $path;
}
}
use lib 'lib';
use Bot::BB3;
use Data::Dumper;
Bot::BB3->new( {
main_conf_file => $OPTS{c} || 'etc/bb3.conf',
plugin_conf_file => $OPTS{p} || 'etc/plugins.conf',
only_roles => $OPTS{m},
} );
# Only daemonize if we're asked to.
if( $OPTS{d} ) {
setsid();
fork and exit;
open STDOUT, ">var/bb3.stdout" or die "Tried to reopen STDOUT to bb3.stdout: $!";
open STDERR, ">var/bb3.stderr" or die "Tried to reopen STDERR to bb3.stdout: $!";
close STDIN;
open my $fh, ">var/bb3.pid" or die "Failed to open pid file: $!";
print $fh $$;
close $fh;
}
POE::Kernel->run;
exit;

View file

@ -1,36 +0,0 @@
#!/usr/bin/perl
use strict;
use Term::ReadLine;
use IO::Socket::INET;
use Getopt::Std;
my %OPTS;
getopts( 'p:', \%OPTS );
my $connect_port = $OPTS{p} || 14401;
my $socket = IO::Socket::INET->new(
PeerHost => '127.0.0.1',
PeerPort => $connect_port,
ReuseAddr => 1,
Proto => 'tcp',
Type => SOCK_STREAM,
) or die "Failed to connect to localhost on $connect_port, try specifying a port with -p";
my $term = Term::ReadLine->new( "BB3 Console" );
my $prompt = "bb3> ";
while( defined( $_ = $term->readline($prompt) ) ) {
print $socket "$_\n";
my $output;
sysread $socket, $output, 4096;
$output =~ s/^CC: //;
print $output, "\n";
}

View file

@ -1,6 +0,0 @@
__END__
http://cpan.mirror.facebook.com/authors/01mailrc.txt.gz
http://cpan.mirror.facebook.com/modules/02packages.details.txt.gz
http://cpan.mirror.facebook.com/modules/03modlist.data.gz

View file

@ -1,21 +0,0 @@
#!/usr/bin/perl
# Guess we're being activated inside bin/, so go up a directory.
BEGIN { if( not -e 'lib' and not -e 'etc' and -e 'bb3' ) { chdir ".."; } }
use lib 'lib';
use EvalServer;
use POSIX qw/setsid/;
# Only daemonize if we're asked to.
if( $ARGV[0] eq '-d' ) {
# Crude daemonization
setsid();
fork and exit;
open STDOUT, ">var/evalserver.stdout" or die "Tried to reopen STDOUT to bb3.stdout: $!";
open STDERR, ">var/evalserver.stderr" or die "Tried to reopen STDERR to bb3.stdout: $!";
close STDIN;
}
EvalServer->start;

View file

@ -1,23 +0,0 @@
use POE::Filter::Reference;
use IO::Socket::INET;
use Data::Dumper;
my $filter = POE::Filter::Reference->new();
while( 1 ) {
print "Code: ";
my $code = <STDIN>;
my $socket = IO::Socket::INET->new( PeerAddr => '127.0.0.1', PeerPort => '14400' );
my $refs = $filter->put( [ { code => "$code" } ] );
print $socket $refs->[0];
local $/;
my $output = <$socket>;
print "OUTPUT: ", Dumper($filter->get( [ $output ] )), "\n";
$socket->close;
}

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,334 +0,0 @@
10
dir
475
svn://erxz.com/bb3/branches/perlbuut/docs
svn://erxz.com/bb3
2009-10-03T22:53:42.528878Z
475
simcop
has-props
dcb1cea6-7f7e-4c78-8a22-148ace8ce36e
module_list
file
2009-10-03T22:54:20.000000Z
b0b9bb1eb56ed498ddae32c4f8c9b82b
2009-10-03T22:53:42.528878Z
475
simcop
526
said
file
2009-10-03T22:54:20.000000Z
23502f33387a03c10ea31dc6a0c40f51
2009-10-03T22:53:42.528878Z
475
simcop
2691
plugins
file
2009-10-03T22:54:20.000000Z
b9336859368d642906a04c17463a09e3
2009-10-03T22:53:42.528878Z
475
simcop
2499
modulelist2
file
2009-10-03T22:54:20.000000Z
29104ecfeafbd90619a0cace9358f49d
2009-10-03T22:53:42.528878Z
475
simcop
1532
writing_plugins
file
2009-10-03T22:54:20.000000Z
85f2cf1c4ac1d09756073a01f4ab3b80
2009-10-03T22:53:42.528878Z
475
simcop
10523
todo
file
2009-10-03T22:54:20.000000Z
036648498679c4c525bed38967ba24c6
2009-10-03T22:53:42.528878Z
475
simcop
214
daemons
file
2009-10-03T22:54:20.000000Z
44a6e62ea707ac1dc48d350b964b0b60
2009-10-03T22:53:42.528878Z
475
simcop
306
pluginmanager
file
2009-10-03T22:54:20.000000Z
4e8751dce708a400d4057bf1f007576e
2009-10-03T22:53:42.528878Z
475
simcop
333
quick_start
file
2009-10-03T22:54:20.000000Z
58e2bbac93cb143d57c77d4c79262170
2009-10-03T22:53:42.528878Z
475
simcop
8628

View file

@ -1,4 +0,0 @@
Long running commands spawned by the bot.
Register for inputs to receieve?
-- Is this necessary? I mean, plugins are pretty sexy now. The primary factoid case has been handled by the new fork/plugin model.
We may only need daemons for sending output on timers or outside events, eg, commits or rss feeds.

View file

@ -1,40 +0,0 @@
CORE
--------
BSD::Resource
Config::General
IO::Socket::INET
JavaScript::SpiderMonkey
Parse::RecDescent
POE
POE::Component::IRC
POE::Wheel::Run
POSIX
Scalar::Util
Storable
Text::Glob
Cache::FastMmap
Memoize
OPTIONAL
------------
CGI
DBD::SQLite
DBI
LWP::Simple
Net::Dict
POE::Filter::Reference
Text::Aspell
WWW::Shorten
WWW::RottenTomatoes
Finance::Quote
WWW::Shorten
XML::RSS::Parser
IMDB
Math::Farnsworth
Module::CoreList
POE::Component::Server::SimpleHTTP
Template
Text::Soundex
Geo::WeatherNWS
Weather::Underground

View file

@ -1,88 +0,0 @@
B::Deparse
Bot::BB3::ConfigParser
Bot::BB3::Logger
Bot::BB3::PluginConfigParser
Bot::BB3::PluginManager
Bot::BB3::PluginWrapper
Bot::BB3::Roles::Console
Bot::BB3::Roles::IRC
Bot::BB3::Roles::PasteBot
Bot::BB3::Roles::Skynet
Bot::BB3::Roles::SocketMessageIRC
Bot::BB3::Roles::Web
Bot::BB3::MacroQuote
BSD::Resource
Carp::Heavy
CGI
charnames
Config::General
Data::Dumper
HTTP::Status
Memoize
Parse::RecDescent
PerlIO
PerlIO::scalar
POE
POE::Component::IRC
POE::Component::IRC::Common
POE::Component::IRC::Plugin::AutoJoin
POE::Component::IRC::Plugin::Connector
POE::Component::IRC::Plugin::NickReclaim
POE::Component::IRC::State
POE::Component::Server::SimpleHTTP
POE::Component::IRC::Plugin
POE::Component::IRC::Constants
POE::Component::IRC::Qnet
POE::Component::IRC::Qnet::State
POE::Filter::Line
POE::Filter::Reference
POE::Filter::Stream
POE::Session
POE::Wheel::ReadWrite
POE::Wheel::Run
POE::Wheel::SocketFactory
POSIX
Scalar::Util
Socket
Storable
strict
Template
Text::Glob
Tie::Hash::NamedCapture
utf8
POE::Pipe
POE::Pipe::TwoWay
POE::Pipe::OneWay
DateTime::Format::Natural::Base
POE::Component::Pluggable::Constants
Bot::BB3::MacroQuote
CGI
Data::Dumper
DBD::SQLite
DBI
Geo::IP
IMDB
IO::Socket::INET
IPC::Open2
LWP::Simple
LWP::UserAgent
Math::Farnsworth
Math::Pari
Module::CoreList
Net::Dict
POE::Component::IRC::Common
POE::Filter::Reference
Scalar::Util
Storable
strict
strict
Text::Aspell
warnings
WWW::RottenTomatoes
WWW::Shorten
WWW::Shorten::Metamark
XML::RSS::Parser
Compress::Zlib
IO::File
IO::Seekable
IO::Handle

View file

@ -1,11 +0,0 @@
Basic specification:
Read plugins.
Fork children.
Monitor children.
Wait for death.
Fork new child.
(Note, children required to die on schedule?)
Pass lines out to children, deal with output, pass output to main bot object.
Pre-fork model similar to apache. Plugin objects initialized after fork. Must not assume persistent state.

View file

@ -1,57 +0,0 @@
A plugin is implemented as a single file in the plugin directory. This is of course named 'plugins' unless otherwise specified in the bot's config file. Each file is executed and the return value (of the file!) is examined. If the return value is a subroutine, then it is taken as a basic plugin that executes in the 'main' phase of plugin handling and has no special configuration. If, on the other hand, the plugin returns a string, this is taken as the name of a package to call the method 'new' on to get an object. This object is then initialized and so forth then called with input in the normal course of plugin handling. Note that objects are still subject to the configuration specified in the plugin.conf file, but however, can do more configuration of how the main bot object interacts with the plugin, specifically when in the plugin loop they're activated and perhaps even a chance to preprocess or postprocess the output of other plugins. This is of course the 'advanced' method. Someday I'll actually document what the interface for all of this actually is.
File: echo.pm
#----------------START--------------
sub {
my( $said ) = @_;
print "You said: $said->{body}";
}
#-----------------EOF---------------
File: morecomplicated.pm
#----------------START--------------
package Bot::BB3::Plugin::Complicated;
sub new {
my( $class ) = @_;
return bless {}, $class;
}
sub initialize {
my( $self ) = @_;
#stuff
}
#Class name to execute
"Bot::BB3::Plugin::Complicated";
#-----------------EOF---------------
In particular note is the string returned, this is the name of the package to invoke. Note that you could use this as a dummy file to invoke modules installed elsewhere on the system, for example:
File: dummymodule.pm
#----------------START--------------
use MyModule::Somewhere;
"MyModule::Somewhere";
#-----------------EOF---------------
This simply calls 'use' to load the module and then returns the name.
Note that plugins who return a coderef are 'wrapped' by invoking Bot::BB3::PluginWrapper->new( $name, $coderef );
This provides the basic example for implementing a plugin object.
More Notes:
Plugins can check if the line has been handled before them.
Plugins activate only if configured to do so.
Plugins handle parsing..
Plugins need constants to return in certain situations.
Plugins have options to not handle the line, thus needing a NOT_HANDLED; constant?
Handled is the default I think?
Every plugin would have to return a constant.. maybe.

View file

@ -1,68 +0,0 @@
The Quick Start Guide.
From a user's perspective, there are three main directories you should be interested in. These are etc/, bin/ and plugins/. More detailed explanations follow:
etc/ is the directory that contains all of the files necessary to configure the bot. At the moment it should only contain two files, bb3.conf and plugins.conf.
plugins.conf use a simple configuration language to control which plugins will respond in what channel and to what 'authorization' level. At the moment the syntax for the configuration language looks something like:
server "*" {
channel "#test" {
plugin "join" { access: op; addressed: true }
}
}
Essentially it's a series of nested filters that apply to the final set of conditions. The first filter is the "server" command, which attempts to match against the server name of whatever irc network generated the message the bot is attempting to reply to. The second filter is "channel" which obviously attempts to match against the channel the message was received on. Finally the "plugin" filter, again, matches against the specific plugin that it's trying to activate.
Note that you can use the '*' wildcard anywhere you could specify a specific string to cause that filter to always match. Also note you can have multiples of any section of the above example, that is, you can have multiple server blocks in the file, you can have multiple channel blocks inside a server block and so on. The configuration options are generally merged in an additive file, so if you have multiple blocks that have different conditions for the same plugin on the same channel on the same server, all of these conditions will have to be matched before it executes.
The final section of the above example is the actual "plugin" section, which again, tries to match against a specific plugin name and then applies the options listed inside the curly braces. In this case the two options being applied are 'access' and 'addressed'. The 'addressed' option controls whether or not the bot needs to have been addressed for this plugin to activate, in this case a value of true means it has to have been addressed, in the form of 'buubot: echo foo'. You can also specify 'false' to do the opposite, this is also useful for using a general filter to cause every plugin to be addressed and then setting specific ones to 'false' to let them respond without being addressed
The other option in the example is 'access' which simply controls what level of "authorization" might be required to activate the plugin. As of now there are only three levels of access, the default one which is what happens when you don't specify an access option, means anyone can use it. The second is 'op' which means you must currently be 'opped' in the channel where you're attempting to use the command. The last acceptable value is 'root' which means your hostmask much match the root_mask value in the etc/bb3.conf.
The second file in the etc/ directory is the bb3.conf file. Note that the bin/bb3 launching command automatically looks for a file named etc/bb3.conf to configure itself.
Obviously, this file contains directives used to configure which networks the bot connects to and under what name, along with root user, people to ignore, and various other plugin or role specific configuration directives. Technically this file uses syntax from the perl module Config::General, but you can think of it as the same language that Apache uses in its conf file, which can be summarized as <Foo bar> option_value 42 </Foo> where <Foo> starts a section named 'bar' and option_value 42 sets the option_value to 42, whatever thay may do.
The main configuration directive is named 'bot' and looks something like:
<bot buubot>
channel \#buubot
channel \#bottest
ignore avarbot
ignore otherbot
server irc.freenode.org
root_mask user@host.org
</bot>
The first section is the <bot buubot> part, which starts a new 'bot' section, which is ended by the corresponding </bot> tag, and specifies that the nickname of this bot, when it connects to the irc server, should be 'buubot'. Because that's an awesome name.
The second section is the 'channel' directive which specifies a list of channels to join as soon as the bot has connected. Each channel directive specifies a single channel, simply repeat it to join multiple. You can specify as many as the irc network will let you connect to. Note that you have to escape the # (\#) signs in the channel name, since this configuration language uses # to begin a comment.
The next section is the 'ignores', these simply take the name of an irc user for the bot to ignore, note that the bot will drop ALL input from any user whose nick matches the name specified. Again, you can specify multiple people to ignore simply by specifying the directive multiple times.
The next directive is really the only required one, and that is the 'server' command, which takes a single argument, the name of an irc server to attempt to connect to. In this case, irc.freenode.org, because I like freenode. Note that unlike the rest of the directives, you can only specify one 'server' directive. If you want to connect to multiple servers at the same time, simply create multiple <bot foo></bot> blocks, each one containing a different server directive.
The last directive is 'root_mask', which determines which users are able to use plugins with an 'access' set to 'root'. It matches fairly directly against your IRC hostmask, to set it to something similar.
There are a number of other configuration directives in the file, which mostly aren't that interesting, with the possible exception of the following:
<plugin_manager>
default_plugin factoids
</plugin_manager>
This block controls which plugin responds when the bot is addressed and it can't find a plugin that matches what the user said.
An example, if our bot was currently named 'buubot' and a user spoke to it as such: "buubot: echo foo bar", assuming you left the default plugin named 'echo' in the directory, buubot would match the 'echo' at the start of the string and then pass the rest of the arguments to the echo plugin. However, if a user spoke to the bot like "buubot: flibble", and assuming you didn't have a plugin named 'flibble' in your plugin directory, then it would activate the plugin specified as the 'default_plugin' in the above configuration section and pass that string to it. In this case, the default_plugin is configured to be 'factoids' which is the plugin that handles learning random strings based on user input, try 'help factoids' once the bot is running for more information.
You could set this default plugin to be any of the plugin in the plugins/directive, for example you might prefer to set it to the 'eval' plugin or anything else, if you prefer the bot to default to that.
Anyway, once you've configured the above files to your liking, you can actually launch the bot with the command bin/bb3. This launches the bot and attempts to get it connected to the irc networks you specified. Note that you can pass the flag '-d' to bin/bb3 to have it attempt to 'daemonize' itself and stop spewing output to your console. Also note that it really expects to be run from the base directory you checked out or downloaded, that is, you should have a folder named something containing bin, etc, plugins, lib and so forth, and you really should run this command from inside that directory with the command 'bin/bb3'.
The second interesting command in the bin directory is the 'evalserver' command which attempts to launch the evalserver. As a quick summary, the evalserver runs as a standalone tcp-socket based server which receives commands to evaluate, evaluates them and returns the output. By default it only listens to localhost connections on the port 14400, unfortunately this port can't be configured at the moment, if you want to change it just edit lib/EvalServer.pm and search for it.
Note that this server must be run as root, so it can perform the necessary security based actions of invoking 'chroot' and dropping its user and group id to the user 'nobody'. You're welcome to think this is moderately unsafe, but the code involved is relatively simple and easy to audit. The two files involved are lib/EvalServer.pm and lib/eval.pl, which performs the actual security based sections. In any case, unless you either find an actual hole or can come up with an alternate way to perform this, please refrain from commenting. I'll note that I've been running this server, as root, for over a year now on highly populated IRC channels, and have received no ill-effects.

View file

@ -1,37 +0,0 @@
The said object is the heart of the bb3 communication system. Specifically it's the only argument passed to plugins of various types. It contains all of the knowledge necessary to respond to a single IRC line (or other forms of communication). The values are as follows:
$said = {
body_raw => The exact text that it was sent
body => The potentially parsed results of the body. Note that several things modify
it at the moment. For example, the name of the bot is removed if it is
found at the start. In some cases, if the leading text matches the name
of a command, it is also removed.
recommended_args => An array ref of words in the text.
Note that this only exists for 'command' type plugins.
It's just a guess to save some time, it's split on whitespace
or similar.
channel => The channel that it was seen in. In the case of IRC this is of the form
#channel, but other roles use channel names starting with *, for example,
*irc_msg or *dcc_send or even *web for the web based interface.
addressed => Whether or not the bot detected its name at the beginning
sender_raw => The raw description of the person who sent the text`
name => The irc nick of the person who sent the line
ircname => The irc username of the sender.
host => The host of the person who sent the text
server => The server it was seen on
pci_id => The POE::Component::IRC ID, obviously internal use only.
my_name => The current name of the bot who saw it.
special_commands => This is an arrayref of arrayrefs, elements that are used as
arguments to $poe_component_irc->yield, if they start with pci_
that is: special_commands => [ [ 'pci_join', '#foo', '#bar' ] ]; causes:
$pci->yield( 'join', => '#foo', '#bar' );
If the command starts with pm_ it calls the specified method on the
$plugin_manager object that executed this plugin. If it starts with
bb3_ it calls the method on the parent bb3 object that spawned the
plugin_manager in the first place.
parent_session => This is added by the plugin_manager to record which session sent
us the $said event in the first place. Mostly internal use.
by_root => Set to 1 if the line was "spoken" by someone who matches the root_mask
by_chan_op => Set to 1 if the line was "spoken" by someone who currently has
operator status in the channel he spoke the line in.
};

View file

@ -1,6 +0,0 @@
Add cron type plugin.
Decide how rss works. Especially with cron.
Create website. Document ways to talk to the bot. Document plugins. Document install.
Modify color based output to try to avoid coloring a file.

View file

@ -1,100 +0,0 @@
Plugins come in two forms. The first form is a simple subroutine ref. The second is a complex object.
In the first case, simply define an annonymous subroutine reference in a file in the appropriate directory (typically the one named 'plugins') with the file name being the name of the command you want. You can do other things other than define an annonymous sub, but make sure that the subref is the last value to be returned. This subroutine ref is encapsulated as part of a larger object and then executed whenever the bot sees a string that matches the name of the file, which is also the command name. The subroutine is passed two arguments, the first is $said, which is documented in docs/said, and the second is the $plugin_manager object, which isn't really documented.
As mentioned, $said contains all of the necessary information for you to respond to the user who triggered your plugin. The most relevant fields of $said are as follows:
body: This is the text sent to the bot. Note that if it was addressed to the bot, by prefixing the sentence with the bot's current name, this is stripped. The command name, which caused this particular plugin to be triggered, is also stripped. That is, if the bot was named 'bb3' and the example command was named 'echo', then the string "bb3: echo: hello world" would generate a body field of "hello world".
raw_body: The exact text sent to the bot, includes bot name, command name, and so on.
recommended_args: This is an array ref of whitespace split text that is supposed to be a decent guess as to how you should interpret the text sent to you. "echo foo bar" would generate a recommended args of ['foo','bar'].
See docs/said for more complete documentation of the $said 'object'.
Simple plugins can use print to communicate back to the caller. They should also be able to return a value from the end of the subroutine that will be displayed to the user.
Plugins also use the __DATA__ handle to store documentation for themselves.
Example, echo.pm:
sub {
my( $said, $pm ) = @_;
print $said->{body};
}
__DATA__
Example echo plugin. Outputs whatever it sees.
--- END OF EXAMPLE ---
There are a couple of key points. The first is the file name, in this case, 'echo.pm'. This filename determines the command name the plugin will by activated against. The .pm suffix is also important since the bot will ignore files that don't end in .pm.
The second is the arguments passed to the sub. All simple plugins take two arguments, $said and $pm. $said is the object mentioned above. $pm is the Bot::BB3::PluginManager object, which is the internal object that handles loading and dispatching to the various plugins. It's mostly useful for accessing configuration information about the bot or for getting references to other plugins. 99% of plugins will never need to use this object. For a moderately simple example of using this object to access other plugins, see plugins/help.pm.
The next point is the 'print' and the and $said->{body}. The routine that activates this plugin used a tied STDOUT to allow 'print' to be used for outputting text, so you can't get too clever. $said->{body} is, as mentioned frequently, the remainder of the text that caused the bot to activate this plugin, minus the bot name and the command name.
The last bit, the __DATA__ section, is of course the documentation for the plugin and is read by plugins/help.pm and possibly other areas in order to provide helpful information to the user.
Complicated plugins, or at least, not simple plugins, allow for much more flexibility in how you define and interact with events from the bot. They're also the only way to respond to every line the bot reads without having to be activated by a specific command string.
These types of plugins need to declare a module/package and return a string that contains the name of the package that the PluginManager will invoke new on to get a plugin object.
The only two required methods for a 'complex' plugin are 'new' and 'command'. new is invoked by the PluginManager as it loads all of the plugins it finds in the plugins/ directory and should return an object. For a variety of mostly silly reasons, at the moment the object has to be of the type 'blessed hash' and contain the following key/value pairs in the hash. If you don't like this, send a patch.
The blessed hash should contain a 'name' field, containing the name that the plugin should be activated by, as well as the 'opts' key, which points to a hashref containing option=>value pairs. There are four supported options that may be present in the 'opts' hash. They are, handler, command, preprocess and postprocess. One or more of these has to be set or the plugin will never be activated. The exact meanings are as follows:
command => 1
This causes the bot to activate your plugin as a simple command, which is incidentally how the above documented 'simple plugins' are implemented. In short it looks for the name of your plugin at the beginning of the string followed by some optional delimiter characters (such as : or ,) and then calls your plugin's ->command method.
handler => 1
This causes the bot to activate your plugin on every line that the bot sees. It calls the method ->handle.
preprocess => 1
This causes your plugin to be invoked on every line the bot sees, before any other plugin is activated. Note that you are simply passed a $said object, but this is the same object that every other plugin will see, so you can modify it at will.
postprocess => 1
Much the same as preprocess, this causes your plugin to be invoked after every other plugin has finished. You are passed $said and a reference to the output of all of the previous plugins that you may modify as you wish.
The command method. This is the method that actually does all the work for your plugin and is invoked by the PluginManager depending on the criteria defined above in the opts hash. It is passed: $self, $said, $pm. $self and $said are your object and the said object obviously and $pm is the PluginManager object as mentioned above. Note that 'handle' options invoke the 'handle' method instead.
This method is expected to return a list of two items. Note that this is a list and not an array reference. This first item should be a string, either 'handled' or an empty string ''. The second item is another string containing the text you wish to output to the user.
Digression: Plugin Chains. When a line is seen by the bot, it creates a plugin chain, that is, a list of all the plugins the bot thinks might be activated for that line, such as commands that match or handlers that always activate and so on. However, in an attempt to ensure that only a single plugin is actually activated per line, each plugin can return either 'handled' or '' as the first item it returns. If the string is 'handled' then the plugin chain is aborted at that point and the output as of that point is the final output from the bot for that line. Note that you can return an empty string as the first item, thus saying you haven't actually handled the line, but still output text by returning it as the second item. This text will be prepended to whatever else is output by other plugins down the line of the chain. Note that all 'simple plugins' always return 'handled' when they're called. Note that 'handler' plugins can't return handled, their output is always concatenated onwards.
Example, FlibbleEcho.pm:
package Bot::BB3::Plugin::FlibbleEcho;
sub new {
my( $class ) = @_;
my $self = bless {}, $class;
$self->{name} = "echo";
$self->{opts} = { command => 1 };
return $self;
}
sub postload {
my( $self, $pm ) = @_;
Create_Database();
}
sub initialize {
my( $self, $pm, $cache ) = @_;
$self->{cache} = $cache;
# Create database handle maybe
}
sub command {
my( $self, $said, $pm ) = @_;
return( 'handled', "Flibble! $said->{body}" );
}
"Bot::BB3::Plugin::FlibbleEcho";
__DATA__
The flibble echo. Prepends flibble to whatever string it sees and returns the string! Responds to echo.
-------- End of Example ----------
Again, there are some interesting points. The first is the package declaration, this is necessary for us to create our perl object later. Note we stick it inside the Bot::BB3::Plugin namespace, this isn't a requirement, but it at least makes a pretense at organization.
The next is the new method where we create the object. The two interesting sub points are the 'name' key which specifies our name and the 'opts' hash which specifies that we should be treated as a 'command', which if you will recall, causes our plugin to be activated when our 'name' is detected at the beginning of a string. Note that despite the fact that the file name is 'FlibbleEcho' we declare our name to be 'echo' and are activated based on that declared name. That is to say, in the case of 'complex plugins', the file name is irrelevant.
At the end obviously we return our self as a standard object creation method.
The postload method is called directly after the plugin is instantiated by the pluginmanager and before the pluginmanager forks off its children. It is useful for performing any 'setup' operations you need done before the plugin is in actual use by the bot.
The initialize method is called on every plugin loaded directly after the pluginmanager forks its children, so it will be called on each plugin for ever child spawned. It varies from the rest of the methods by being passed a $cache object which conforms to the Cache interface. Note that every plugin gets the same cache object, even across forks. This is useful for IPC and storing temporary values. For example, the 'more' plugin is implemented by saving the data in this cache.
The command subroutine is very simple in this example, we simply assign our arguments to local variables and then return. We return a string of 'handled' to denote the fact that we've successfully responded to this command and then the text we want to output, which in this case is the string Flibble! prepended to whatever text the user has written.
At the end of the plugin we return a string, which should be the name of the package we declared at the beginning. This is the package name that the PluginManager will call new on to create this object for this plugin. Note that you could actually simply 'use' another package and then return the name of that package, instead of actually implementing the entire plugin in side this file.
Again at the very end is the __DATA__ section which contains our help text.

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,96 +0,0 @@
10
dir
475
svn://erxz.com/bb3/branches/perlbuut/etc
svn://erxz.com/bb3
2009-10-03T22:53:42.528878Z
475
simcop
has-props
dcb1cea6-7f7e-4c78-8a22-148ace8ce36e
plugins.conf
file
487
2009-10-05T14:25:14.000000Z
7c9758bae26f06475cb3988ea75c2548
2009-11-24T01:05:35.791810Z
487
simcop
388
bb3.conf
file
487
2009-11-24T00:47:29.000000Z
6ecd3ff71fb0c9faf09d38c61f75435c
2009-11-24T01:05:35.791810Z
487
simcop
1416

View file

@ -1,83 +0,0 @@
<be>
# change this to point to the actual directory where you store quotes
quotes_dir /home/buu/p/bb2/trunk/quotes
</be>
http_plugin_port 1092
<Roles>
<socketmessageirc>
enabled no
port 10091
</socketmessageirc>
<console>
enabled no
port 10092
</console>
<web>
enabled no
</web>
<pastebot>
enabled no
hostname erxz.com:10081
alias_url = http://erxz.com/bb3pb
</pastebot>
</Roles>
# Change this to be bot specific?
<plugin_manager>
default_plugin factoids
</plugin_manager>
<bot perlbot>
channel \#buubot
channel \#\#turtles
channel \#perlcafe
channel \#webgui
channel \#citadel
channel \#modperl
channel \#perl
channel \#ipv6
channel \#perlbot
channel \#mrtg
channel \#ipv6-fr
channel \#freebsd-fr
channel \#botpark
channel \#css
channel \#modus
ignore buubot
ignore avarbot
ignore jeval
ignore gumbybrain
ignore perlbot
ignore buubot3
ignore loudbot
ignore serfbot
ignore farnsworth
ignore frogbot
ignore EvanCarroll
ignore EvanCarrol
ignore EvanCaroll
ignore EvanCarol
ignore EC
server localhost
username perlbuutfreenode
password perlbuut
port 6668
root_mask n=simcop23@p3m/member/simcop2387
</bot>
<bot perlbot>
channel \#freenode-perl-cabal
server localhost
username perlbuutmagnet
password perlbuut
port 6668
root_mask ~simcop238@erxz.com
</bot>

View file

@ -1,13 +0,0 @@
server "*" {
channel "*" {
plugin "*" { addressed: true }
plugin "join" { access: root; addressed: true }
plugin "part" { access: op }
plugin "reload_plugins" { access: root }
plugin "restart" { access: root }
plugin "conf_dump" { access: root; }
plugin "save_config" { access: root; }
plugin "conf" { access: root; }
plugin "karma_modify" { addressed: false; }
}
}

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,102 +0,0 @@
10
dir
475
svn://erxz.com/bb3/branches/perlbuut/lib
svn://erxz.com/bb3
2009-10-03T22:53:42.528878Z
475
simcop
has-props
dcb1cea6-7f7e-4c78-8a22-148ace8ce36e
jail
dir
eval.pl
file
2009-10-03T22:54:20.000000Z
1cd271f250952a825ac64590a5db7682
2009-10-03T22:53:42.528878Z
475
simcop
has-props
7271
Bot
dir
EvalServer.pm
file
2009-10-03T22:54:20.000000Z
89cccf6bbe65af76f437521edace7a6d
2009-10-03T22:53:42.528878Z
475
simcop
5460

View file

@ -1,5 +0,0 @@
K 14
svn:executable
V 1
*
END

View file

@ -1,238 +0,0 @@
package EvalServer;
use POE;
use POE::Wheel::SocketFactory;
use POE::Wheel::ReadWrite;
use POE::Filter::Reference;
use POE::Filter::Line;
use POE::Filter::Stream;
use POE::Wheel::Run;
use strict;
sub start {
my( $class ) = @_;
my $self = $class->new;
my $session = POE::Session->create(
object_states => [
$self => [ qw/
_start _stop
socket_new socket_fail socket_read socket_write
spawn_eval eval_read eval_err eval_close eval_stdin
dead_child timeout
/ ],
]
);
POE::Kernel->run();
}
sub new {
return bless {}, shift;
}
sub spawn_eval {
my( $self, $kernel, $args, $parent_id ) = @_[OBJECT,KERNEL,ARG0,ARG1];
my $filename = 'eval.pl';
if( not -e $filename ) {
$filename = "lib/$filename";
}
warn "Spawning Eval: $args->{code}\n";
my $wheel = POE::Wheel::Run->new(
Program => [ 'perl', $filename ],
ProgramArgs => [ ],
CloseOnCall => 1, #Make sure all of the filehandles are closed.
Priority => 10, #Let's be nice!
StdoutEvent => 'eval_read',
StderrEvent => 'eval_err',
StdinEvent => 'eval_stdin',
CloseEvent => 'eval_close',
StdinFilter => POE::Filter::Line->new,
StdoutFilter => POE::Filter::Stream->new(),
StderrFilter => POE::Filter::Stream->new(),
);
warn "Storing Eval id: ", $wheel->ID, "\n";
$self->{ eval_wheels }->{ $wheel->ID } = { wheel => $wheel, parent_id => $parent_id };
$wheel->put( $args->{code} );
warn "Adding delay for 12 seconds: ", $wheel->ID;
$kernel->delay_set( timeout => 12, $wheel->ID );
}
sub timeout {
my( $self, $wheel_id ) = @_[OBJECT,ARG0];
warn "Got a timeout idea for $wheel_id";
my $wheel = $self->{ eval_wheels }->{ $wheel_id }->{ wheel }
or return; # Our wheel has gone away already.
warn "Trying to kill: ", $wheel->PID;
kill( 9, $wheel->PID );
}
sub _append_output {
my $self = shift; #Decrement @_ !
my( $cur_session, $kernel, $results, $id ) = @_[SESSION,KERNEL,ARG0,ARG1];
warn "AT UNDERSCORE: @_\n";
warn "Attempting to append: $self, $results, $id\n";
#return unless $results =~ /\S/;
my $output_buffer = $self->{ wheel_outputs }->{ $id } ||= [];
push @$output_buffer, $results;
warn "Checking length: ", scalar( @$output_buffer );
if( @$output_buffer > 1000 ) { # Lets not be silly
warn "Attempting to force a timeout using $cur_session";
$kernel->call( $cur_session->ID, timeout => $id ); #Force a timeout. Go away spammy outputs.
my $wheel = $self->{ eval_wheels }->{ $id }->{ wheel };
if( $wheel ) { $wheel->pause_stdout };
$kernel->call( $cur_session->ID, eval_close => $id );
}
}
sub eval_read {
#my( $self, $cur_session, $kernel, $results, $id ) = @_[OBJECT,SESSION,KERNEL,ARG0,ARG1];
my( $self ) = @_[OBJECT];
$self->_append_output( @_ );
}
sub eval_err {
my( $self, $error ) = @_[OBJECT,ARG0];
$self->_append_output( @_ );
}
sub eval_stdin {
my( $self, $id ) = @_[OBJECT,ARG0];
warn "STDIN EVENT\n";
#We've successfully flushed our output to the eval child
#so shutdown the wheel's stdin
my $wheel = $self->{ eval_wheels }->{ $id }->{ wheel};
$wheel->shutdown_stdin;
}
sub eval_close {
my( $self, $id ) = @_[OBJECT,ARG0];
warn "CLOSE EVENT\n";
# Sorry.
# I should find a better way someday.
warn "Looking for id: $id\n";
my $wheel_struct = delete $self->{ eval_wheels }->{ $id };
return unless $wheel_struct;
# Get our parent's ID
my $parent_id = $wheel_struct->{ parent_id };
warn "Found parent: $parent_id\n";
my $parent_wheel = $self->{ socket_wheels }->{ $parent_id };
# Send the results back to our client
my $outputs = delete $self->{ wheel_outputs }->{ $id };
warn "Close, my outputs: ", Dumper( $outputs );
# Not sure how we end up without a $parent_wheel, but we shouldn't die
if( $parent_wheel ) {
if( $outputs and @$outputs ) {
$parent_wheel->put( [ join '', @$outputs ] );
}
else {
$parent_wheel->put( [ ] );
}
}
}
sub _start {
my( $self, $kernel ) = @_[OBJECT,KERNEL];
warn "Eval Server starting\n";
$self->{socket_factory} = POE::Wheel::SocketFactory->new(
BindAddress => "127.0.0.1",
BindPort => '14400',
SuccessEvent => 'socket_new',
FailureEvent => 'socket_fail',
Reuse => 'on',
);
warn "Ready for connections...\n";
$kernel->sig( 'CHLD' => 'dead_child' );
}
sub socket_new {
my( $self, $handle ) = @_[OBJECT,ARG0];
warn "Got a socket\n";
my $wheel = POE::Wheel::ReadWrite->new(
Handle => $handle,
Driver => POE::Driver::SysRW->new(),
Filter => POE::Filter::Reference->new(),
InputEvent => 'socket_read',
FlushedEvent => 'socket_write',
ErrorEvent => 'socket_error',
);
warn "Storing socket as : ", $wheel->ID, "\n";
$self->{socket_wheels}->{ $wheel->ID } = $wheel;
}
sub socket_fail {
warn "SOCKET FAIL: $_[ARG0],$_[ARG1]\n";
}
sub socket_read {
my( $object, $kernel, $input, $wheel_id ) = @_[OBJECT,KERNEL,ARG0,ARG1];
use Data::Dumper;
warn "Got Input: ", Dumper $input;
$kernel->yield( spawn_eval => $input, $wheel_id );
}
sub socket_write {
my( $self, $id ) = @_[OBJECT,ARG0];
warn "SOCKET_WRITE!\n";
# We've received our single chunk of output for this
# response so remove the wheel.
my $wheel = delete $self->{socket_wheels}->{ $id };
$wheel->shutdown_input();
$wheel->shutdown_output();
}
sub socket_error {
my( $self, $id ) = @_[OBJECT,ARG0];
warn "Socket failed!\n";
delete $self->{socket_wheels}->{ $id };
}
sub _stop {
}
sub dead_child {
#Do nothing
#Side effect is the child is already reaped
}
1;

View file

@ -1,319 +0,0 @@
#!/usr/bin/perl
use strict;
use Data::Dumper;
use Scalar::Util; #Required by Data::Dumper
use BSD::Resource;
use File::Glob;
use POSIX;
# This sub is defined here so that it is defined before the 'use charnames'
# command. This causes extremely strange interactions that result in the
# deparse output being much longer than it should be.
sub deparse_perl_code {
my( $code ) = @_;
my $sub = eval "no strict; no warnings; no charnames; sub{ $code\n }";
if( $@ ) { print "Error: $@"; return }
my $dp = B::Deparse->new("-p", "-q", "-x7");
my $ret = $dp->coderef2text($sub);
$ret =~ s/\{//;
$ret =~ s/package (?:\w+(?:::)?)+;//;
$ret =~ s/ no warnings;//;
$ret =~ s/\s+/ /g;
$ret =~ s/\s*\}\s*$//;
print $ret;
}
use utf8; eval "\$\343\201\257 = 42"; # attempt to automatically load the utf8 libraries.
use charnames qw(:full);
use PerlIO;
use PerlIO::scalar;
# Required for perl_deparse
use B::Deparse;
# Javascript Libs
BEGIN{ eval "use JavaScript::SpiderMonkey;"; }
my $JSENV_CODE = do { local $/; open my $fh, "deps/env.js"; <$fh> };
require 'bytes_heavy.pl';
use Tie::Hash::NamedCapture;
uc "\x{666}"; #Attempt to load unicode libraries.
binmode STDOUT, ":utf8"; # Enable utf8 output.
BEGIN{ eval "use PHP::Interpreter;"; }
# Evil Ruby stuff
BEGIN{ eval "use Ruby qw/rb_eval/;"; }
BEGIN { $SIG{SEGV} = sub { die "Segmentation Fault\n"; } } #Attempt to override the handler Ruby installs.
# Evil K20 stuff
BEGIN {
local $@;
eval "use Language::K20;";
unless( $@ ) {
Language::K20::k20eval( "2+2\n" ); # This eval loads the dynamic components before the chroot.
# Note that k20eval always tries to output to stdout so we
# must end the command with a \n to prevent this output.
}
}
BEGIN { chdir "var/"; $0="../$0"; } # CHDIR to stop inline from creating stupid _Inline directories everywhere
# Inline::Lua doesn't seem to provide an eval function. SIGH.
BEGIN { eval 'use Inline Lua => "function lua_eval(str) return loadstring(str) end";'; }
BEGIN { chdir ".."; $0=~s/^\.\.\/// } # Assume our earlier chdir succeded. Yay!
# Evil python stuff
BEGIN { eval "use Inline::Python qw/py_eval/;"; }
# Evil J stuff
BEGIN { eval "use Jplugin;"; }
use Carp::Heavy;
use Storable qw/nfreeze/; nfreeze([]); #Preload Nfreeze since it's loaded on demand
my $code = do { local $/; <STDIN> };
# Close every other filehandle we may have open
# this is probably legacy code at this point since it was used
# inside the original bb2 which forked to execute this code.
opendir my $dh, "/proc/self/fd" or die $!;
while(my $fd = readdir($dh)) { next unless $fd > 2; POSIX::close($fd) }
# Get the nobody uid before we chroot.
my $nobody_uid = getpwnam("nobody");
die "Error, can't find a uid for 'nobody'. Replace with someone who exists" unless $nobody_uid;
# Set the CPU LIMIT.
# Do this before the chroot because some of the other
# setrlimit calls will prevent chroot from working
# however at the same time we need to preload an autload file
# that chroot will prevent, so do it here.
setrlimit(RLIMIT_CPU, 10,10);
# Root Check
if( $< != 0 )
{
die "Not root, can't chroot or take other precautions, dying\n";
}
# The chroot section
chdir("./jail") or
do {
mkdir "./jail";
chdir "./jail" or die "Failed to find a jail live in, couldn't make one either: $!\n";
};
chroot(".") or die $!;
# Here's where we actually drop our root privilege
$)="$nobody_uid $nobody_uid";
$(=$nobody_uid;
$<=$>=$nobody_uid;
POSIX::setgid($nobody_uid); #We just assume the uid is the same as the gid. Hot.
die "Failed to drop to nobody"
if $> != $nobody_uid
or $< != $nobody_uid;
my $kilo = 1024;
my $meg = $kilo * $kilo;
my $limit = 50 * $meg;
(
setrlimit(RLIMIT_DATA, $limit, $limit )
and
setrlimit(RLIMIT_STACK, $limit, $limit )
and
setrlimit(RLIMIT_NPROC, 1,1)
and
setrlimit(RLIMIT_NOFILE, 0,0)
and
setrlimit(RLIMIT_OFILE, 0,0)
and
setrlimit(RLIMIT_OPEN_MAX,0,0)
and
setrlimit(RLIMIT_LOCKS, 0,0)
and
setrlimit(RLIMIT_AS,$limit,$limit)
and
setrlimit(RLIMIT_VMEM,$limit, $limit)
and
setrlimit(RLIMIT_MEMLOCK,100,100)
and
setrlimit(RLIMIT_CPU, 10,10)
)
or die "Failed to set rlimit: $!";
#setrlimit(RLIMIT_MSGQUEUE,100,100);
die "Failed to drop root: $<" if $< == 0;
close STDIN;
$code =~ s/^\s*(\w+)\s*//
or die "Failed to parse code type! $code";
my $type = $1;
# Chomp code..
$code =~ s/\s*$//;
# Choose which type of evaluation to perform
# will probably be a dispatch table soon.
if( $type eq 'perl' or $type eq 'pl' ) {
perl_code($code);
}
elsif( $type eq 'javascript' ) {
javascript_code($code);
}
elsif( $type eq 'php' ) {
php_code($code);
}
elsif( $type eq 'deparse' ) {
deparse_perl_code($code);
}
elsif( $type eq 'k20' ) {
k20_code($code);
}
elsif( $type eq 'rb' or $type eq 'ruby' ) {
ruby_code($code);
}
elsif( $type eq 'py' or $type eq 'python' ) {
python_code($code);
}
elsif( $type eq 'lua' ) {
lua_code($code);
}
elsif( $type eq 'j' ) {
j_code($code);
}
exit;
#-----------------------------------------------------------------------------
# Evaluate the actual code
#-----------------------------------------------------------------------------
sub perl_code {
my( $code ) = @_;
local $@;
local @INC;
local $_;
$code = "no strict; no warnings; package main; $code";
my $ret = eval $code;
local $Data::Dumper::Terse = 1;
local $Data::Dumper::Quotekeys = 0;
local $Data::Dumper::Indent = 0;
local $Data::Dumper::Useqq = 1;
my $out = ref($ret) ? Dumper( $ret ) : "" . $ret;
print $out;
if( $@ ) { print "ERROR: $@" }
}
sub javascript_code {
my( $code ) = @_;
local $@;
my $js = JavaScript::SpiderMonkey->new;
$js->init;
# This is great evil!
JavaScript::SpiderMonkey::JS_ForceLatest( $js->{context} );
# Set up the Environment for ENVJS
$js->property_by_path("Envjs.profile", 0);
$js->function_set("print", sub { print @_ } );
for( qw/log debug info warn error/ ) {
$js->eval("Envjs.$_=function(x){}");
}
$js->eval($JSENV_CODE) or die $@;
my $modified_code = qq!
var ret = eval("\Q$code\E");
if( ret === null ) {
"null"
}
else if( typeof ret == "object" || typeof ret == "array" ) {
ret.toSource();
}
else { ret }
!;
print $js->ret_eval($modified_code);
if( $@ ) { print "ERROR: $@"; }
}
sub ruby_code {
my( $code ) = @_;
local $@;
print rb_eval( $code );
}
sub php_code {
my( $code ) = @_;
local $@;
#warn "PHP - [$code]";
my $php = PHP::Interpreter->new;
$php->set_output_handler(\ my $output );
$php->eval("$code;");
print $php->get_output;
#warn "ENDING";
if( $@ ) { print "ERROR: $@"; }
}
sub k20_code {
my( $code ) = @_;
$code =~ s/\r?\n//g;
Language::K20::k20eval( '."\\\\r ' . int(rand(2**31)) . '";' . "\n"); # set random seed
Language::K20::k20eval( $code );
}
sub python_code {
my( $code ) = @_;
py_eval( $code, 2 );
}
sub lua_code {
my( $code ) = @_;
#print lua_eval( $code )->();
my $ret = lua_eval( $code );
print ref $ret ? $ret->() : $ret;
}
sub j_code {
my( $code ) = @_;
Jplugin::jplugin( $code );
}

View file

@ -1,65 +0,0 @@
10
dir
475
svn://erxz.com/bb3/branches/perlbuut/lib/Bot
svn://erxz.com/bb3
2009-10-03T22:53:42.528878Z
475
simcop
dcb1cea6-7f7e-4c78-8a22-148ace8ce36e
BB3.pm
file
2009-10-03T22:54:20.000000Z
f2b19e514106047a357d6bc11cfaf54b
2009-10-03T22:53:42.528878Z
475
simcop
6143
BB3
dir

View file

@ -1,280 +0,0 @@
package Bot::BB3;
use POE;
use POE::Session;
use POE::Wheel::Run;
use POE::Filter::Reference;
use Memoize qw/memoize/;
use Bot::BB3::ConfigParser;
use Bot::BB3::PluginManager;
use Bot::BB3::PluginConfigParser;
use Bot::BB3::Logger;
use Data::Dumper;
use strict;
sub new {
my( $class, $args ) = @_;
$args->{main_conf_file} ||= 'etc/bb3.conf';
$args->{plugin_conf_file} ||= 'etc/plugins.conf';
my $self = bless { args => $args }, $class;
$self->parse_main_conf( $args->{main_conf_file} );
$self->parse_plugin_conf( $args->{plugin_conf_file} );
$self->_initialize();
$self->_spawn_plugin_manager(); #Must be before spawn_pci, sigh
$self->_spawn_roles( $args->{only_roles} );
return $self;
}
#------------------------------------------------------------------------------
# Init methods
#------------------------------------------------------------------------------
sub _initialize {
my( $self ) = @_;
# WE create a session solely to register for
# a SIGINT handler, this is suboptimal
# but I don't know if it's required.
POE::Session->create(
object_states => [
$self => [qw/_start SIGINT/]
]
);
return 1;
}
sub _spawn_plugin_manager {
my( $self ) = @_;
$self->{ plugin_manager } = Bot::BB3::PluginManager->new(
$self->{ conf },
$self->{ plugin_conf },
$self,
);
}
sub _spawn_roles {
my( $self, $role_list ) = @_;
my $conf = $self->get_conf;
if( $role_list ) {
for( split /\s*,\s*/, $role_list ) { #We should never have spaces anyway
$self->_load_role( $_ ); # I hope they passed the correct module name..
}
}
else { #Load every Role we can find
for my $inc ( @INC ) {
for( glob "$inc/Bot/BB3/Roles/*" ) {
next unless s/\.pm$//;
s/^\Q$inc\///;
s{/}{::}g;
my $role_name = $_;
$role_name =~ s/^Bot::BB3::Roles:://;
warn "Role Name: $role_name";
warn "enabled: $conf->{roles}->{lc $role_name}->{enabled}\n";
unless(
exists $conf->{roles}->{lc $role_name}->{enabled}
and not $conf->{roles}->{lc $role_name}->{enabled}
) {
$self->_load_role( $_ );
}
}
}
}
}
sub _load_role {
my( $self, $role ) = @_;
warn "Attempting to load $role\n";
local $@;
eval "require $role;"; # Avoid having to turn Foo::Bar back in to Foo/Bar.pm..
if( $@ ) { warn "Failed to load $role: $@\n"; return; }
warn "Spawning $role\n";
$self->{$role} = $role->new(
$self->get_conf,
$self->{plugin_manager}, #Hack, maybe.. plugin_manager needs to be loaded first.
);
}
#------------------------------------------------------------------------------
# Public Methods
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Accessors
#------------------------------------------------------------------------------
sub get_conf {
$_[0]->{conf};
}
sub restart {
if( not -x $^X ) {
error "Can't restart ourself because [$^X], our executable, is no longer executable!";
return;
}
if( not -e $0 or not -r _ or -s _ < 100 ) {
error "Can't restart ourself because our bot probram [$0] is no longer in a useful state!";
return;
}
exec $^X, $0, @ARGV;
}
sub parse_main_conf {
my( $self, $conf_file ) = @_;
debug "Parsing config file [$conf_file]";
if( not -e $conf_file or not -r _ ) {
error "Failed to read conf_file [$conf_file]";
exit 1;
}
my $conf = Bot::BB3::ConfigParser->parse_file( $conf_file )
or die "Failed to parse a conf file! $BB3::Conf::PARSE_ERROR\n";
unless( keys %$conf ) {
error "Failed to successfully read [$conf_file]!";
exit 1;
}
my %conf_defaults = (
start_plugin_children => 4,
max_plugin_children => 10,
child_handle_count => 100,
http_plugin_port => 10080,
pastebot_plugin_port => 10081,
);
for( keys %conf_defaults ) {
#Sigh, where is my //=
$conf->{$_} = $conf_defaults{ $_ }
unless defined( $conf->{$_} );
}
debug Dumper $conf;
$self->{conf} = $conf;
return 1;
}
sub parse_plugin_conf {
my( $self, $conf_file ) = @_;
my $conf = Bot::BB3::PluginConfigParser->parse_file( $conf_file )
or die "Failed to parse Plugin Config File: $!\n";
$self->{plugin_conf} = $conf;
}
sub save_main_conf {
my( $self ) = @_;
my $conf = $self->get_conf;
my $conf_filename = $self->{args}->{main_conf_file};
if( not $conf_filename or not -e $conf_filename or not -w _ ) {
error "Couldn't find a valid file to write our conf to, tried [$conf_filename], " .
" either doesn't exist or not writable";
return;
}
Bot::BB3::ConfigParser->save_file( $conf_filename, $conf );
}
sub change_conf {
my( $self, $path, $value ) = @_;
my $conf = $self->get_conf;
if( $value =~ s/^\s*\[\s*// and $value =~ s/\s*\]\s*$// ) {
$value = [ split /\s*,\s*/, $value ];
}
warn "Change_conf initiated";
my $ref = $conf;
my @parts = split /\./, $path;
my $final_key = pop @parts;
for( @parts ) {
if( ref $ref eq 'HASH' ) {
$ref = $ref->{$_};
}
elsif( ref $ref eq 'ARRAY' ) {
$ref = $ref->[$_];
}
else {
error "Passed a path that didn't lead us properly down the rabbit hole. $path";
return;
}
}
warn "change_conf $ref -> $final_key -> $value";
return unless $ref and $final_key and length $value;
warn "Set something: ", $ref->{$final_key} = $value;
warn "New value: $ref->{$final_key}";
use Data::Dumper;
warn Dumper $ref;
warn Dumper $conf;
}
#------------------------------------------------------------------------------
# POE Methods
#------------------------------------------------------------------------------
sub _start {
my( $self, $kernel, $session ) = @_[OBJECT,KERNEL,SESSION];
$kernel->sig( INT => 'SIGINT' );
}
# This is called by a sigints. We ask the plugin manager
# to kill its children. We have to yield a exit call
# otherwise the plugin_manager's yield won't get processed.
# The above issue should be fixed by switching to the
# kernel->call interface. Delete this comment when verified.
sub SIGINT {
my( $self, $kernel ) = @_[OBJECT,KERNEL];
warn "Oh gads, SIGINT\n";
$self->{plugin_manager}->call( 'please_die' );
$kernel->stop;
exit;
}
1;

View file

@ -1,235 +0,0 @@
10
dir
475
svn://erxz.com/bb3/branches/perlbuut/lib/Bot/BB3
svn://erxz.com/bb3
2009-10-03T22:53:42.528878Z
475
simcop
dcb1cea6-7f7e-4c78-8a22-148ace8ce36e
Roles
dir
PluginManager.pm
file
476
2009-10-03T22:57:00.000000Z
3751182102a375087e40cca2bdf0ac29
2009-10-03T22:56:07.617451Z
476
simcop
17890
Logger.pm
file
2009-10-03T22:54:20.000000Z
49cf6477a067563ce902030fce806a5e
2009-10-03T22:53:42.528878Z
475
simcop
1128
MacroQuote.pm
file
2009-10-03T22:54:20.000000Z
2b55ae7ecca3fa5aff53016230c180fe
2009-10-03T22:53:42.528878Z
475
simcop
695
PluginWrapper.pm
file
2009-10-03T22:54:20.000000Z
663f0da688e8e2ebda03cdd14057b2b2
2009-10-03T22:53:42.528878Z
475
simcop
830
PluginConfigParser.pm
file
2009-10-03T22:54:20.000000Z
fcb4c41a5af270686e2b889049d1ada5
2009-10-03T22:53:42.528878Z
475
simcop
1097
ConfigParser.pm
file
2009-10-03T22:54:20.000000Z
6fc5957258ffdc3d5dee40f558545fd5
2009-10-03T22:53:42.528878Z
475
simcop
1546

View file

@ -1,66 +0,0 @@
package Bot::BB3::ConfigParser;
use Config::General;
use Bot::BB3::Logger;
use strict;
sub parse_file {
my( $class, $file ) = @_;
my $conf = {
Bot::BB3::ConfigParser->get_cg_object($file)->getall
};
# This is attempting to distinguish between the options such as:
# <bot MyBotName> </bot>
# and <bot> botname MyBotName </bot>
# type configurations also it handles either multiple
# bots or a single bot. Stupid config general.
# The ->botname bit is a check to make sure we're not dealing with
# a single <bot></bot> defined.
if( $conf->{bot} and ref $conf->{bot} eq 'HASH' and not $conf->{bot}->{botname} ) {
my $bots = $conf->{bot};
my @connections;
while( my( $botname, $options ) = each %$bots )
{
# More attempts at making Config::General behave itself.
next unless ref $options;
for my $options ( ref $options eq 'ARRAY' ? @$options : $options )
{
$options->{botname} = $botname;
push @connections, $options;
}
}
$conf->{bot} = \@connections;
}
# Again, specifically dealing with <bot></bot> thingy. SIGH.
elsif( $conf->{bot}->{botname} ) {
$conf->{bot} = [ $conf->{bot} ];
}
return $conf;
}
sub save_file {
my( $class, $filename, $conf ) = @_;
my $obj = Bot::BB3::ConfigParser->get_cg_object;
# Note that we tend to lose comments doing this..
$obj->save_file( $filename, $conf );
}
sub get_cg_object {
my( $class, $file ) = @_;
return Config::General->new(
-ConfigFile => $file,
-LowerCaseNames => 1,
-UseApacheInclude => 1,
-AutoTrue => 1
);
}
1;

View file

@ -1,64 +0,0 @@
package Bot::BB3::Logger;
use strict;
use Term::ANSIColor qw/:constants/;
sub import {
my( $class ) = @_;
my $calling_package = caller;
no strict;
for( qw/debug log warn error/ ) {
*{"${calling_package}::$_"} = \&$_;
}
}
# goto &foo; automatically calls foo and passes it @_.
# it also removes the current subroutine from the callstack
# and yes I mostly do it for amusment.
sub debug {
unshift @_, 'debug';
goto &write_message;
}
sub log {
unshift @_, 'log';
goto &write_message;
}
sub warn {
unshift @_, 'warn';
goto &write_message;
}
sub error {
unshift @_, 'error';
goto &write_message;
}
my %COLOR_MAP = (
error => RED,
warn => YELLOW,
log => CYAN,
debug => MAGENTA,
);
sub write_message {
my( $level, @message ) = @_;
my( $package, $filename, $line, $sub ) = caller(1); # Ignore the rest of the args
my $message = "@message";
$sub =~ s/^${package}:://;
my $level_color = $COLOR_MAP{$level};
my $reset = RESET;
my $white = WHITE; # This is actually sort of gray..
# Default output
print STDERR "[$level_color$level$reset] $white$package - $line - $sub$reset: $message\n";
}
1;

View file

@ -1,20 +0,0 @@
# This package defines the quoting methods common between the
# quote and arg plugins.
package Bot::BB3::MacroQuote;
sub quote {
my($m,$s) = @_;
if ("z" eq $m) { # no-op
return $s;
} elsif ("c" eq $m || "d" eq $m) { # c-like quoting (without or with double-quote delimiter)
$s =~ s/([\x00\x01\n\r\x10\"\#\$\'\@\\])/sprintf"\\x%02x",ord$1/ge;
return "d" eq $m ? qq["$s"] : $s;
} elsif ("e" eq $m || "f" eq $m) { # quote almost everything
$s =~ s/(\W)/sprintf"\\x%02x",ord$1/ge;
return "f" eq $m ? qq["$s"] : $s;
} elsif ("h" eq $m) { # pack byte to two hex digits each, if nothing else this must work
return unpack "H*", $s;
} else { # unknown quoting mode
return $s;
}
}
1;

View file

@ -1,47 +0,0 @@
package Bot::BB3::PluginConfigParser;
use strict;
use Parse::RecDescent;
use Bot::BB3::Logger;
local $/;
my $grammar = <DATA>;
my $parser = Parse::RecDescent->new( $grammar );
sub parse_file {
my( $package, $filename ) = @_;
open my $fh, "< $filename" or die "$filename: $!";
local $/;
my $filecontents = <$fh>;
$parser->start( $filecontents );
}
1;
__DATA__
start: server(s)
{ $item[1] }
server: 'server' server_name '{' channel(s) '}'
{ [ $item[1], $item[2], $item[4] ] }
channel: 'channel' channel_name '{' plugin(s) '}'
{ [ $item[1], $item[2], $item[4] ] }
plugin: 'plugin' plugin_name '{' option(s?) '}'
{ [ $item[1], $item[2], { map @$_, @{$item[4]} } ] }
option: key ':' value semicolon(?)
{ [$item[1], $item[3]] }
semicolon: ';'
server_name: quoted_string | /[\w.]+/ | '*'
{ $item[1] }
channel_name: quoted_string | /#\w+/ | '*'
{ $item[1] }
plugin_name: quoted_string | /\w+/ | '*'
{ $item[1] }
key: quoted_string | /\w+/
{ $item[1] }
value: quoted_string | /\w+/
{ $item[1] }
quoted_string: /"[^"]+"/
{ my $str = $item[1]; $str =~ s/^"//; $str =~ s/"$//; $str }

View file

@ -1,771 +0,0 @@
package Bot::BB3::PluginManager;
use Bot::BB3::PluginWrapper;
use Bot::BB3::Logger;
use POE;
use Data::Dumper;
use Text::Glob qw/match_glob/;
use Memoize;
use strict;
sub new {
my( $class, $main_conf, $plugin_conf, $bb3 ) = @_;
my $self = bless {
main_conf => $main_conf,
plugin_conf => $plugin_conf,
bb3 => $bb3, # A bit hacky, only used for special commands from plugin at the moment
}, $class;
$self->{child_cache} = $self->create_cache;
$self->_load_plugins();
$self->{session} = POE::Session->create(
object_states => [
$self => [ qw/
_start execute_said handle_said_queue adjust_child_count
please_die child_flushed child_output child_err child_fail
child_close child_die child_time_limit
/ ]
]
);
return $self;
}
#---------------------
# Helpers and accessors
#---------------------
sub yield {
my ( $self, $event, @args ) = @_;
warn "YIELD CALLED: $event\n";
return POE::Kernel->post( $self->{session}, $event, @args );
}
sub call {
my ( $self, $event, @args ) = @_;
warn "CALL CALLED: $event\n";
return POE::Kernel->call( $self->{session}, $event, @args );
}
sub get_main_conf {
return $_[0]->{main_conf};
}
sub get_plugin_conf {
return $_[0]->{plugin_conf};
}
sub get_plugins {
my( $self ) = @_;
return $self->{plugins};
}
sub get_plugin {
my( $self, $name ) = @_;
# Loops are cool.
# O(n) but nobody cares because it's rarely used.
# HA HA THIS IS A LIE.
for( @{ $self->{plugins} } ) {
if( $name eq $_->{name} ) {
return $_;
}
if( $_->{aliases} ) {
for my $alias ( @{ $_->{aliases} } ) {
return $_ if $name eq $alias;
}
}
}
return;
}
memoize( 'get_plugin' ); #Fixes that pesk O(n) thingy.
sub kill_children {
my( $self ) = @_;
for( values %{ $self->{children} } ) {
warn "KILLING: ", $_->{wheel}->PID, ": ",
kill( 9, $_->{wheel}->PID ); #DIE DIE DIE
}
}
sub reload_plugins {
my( $self ) = @_;
delete $self->{plugins};
$self->_load_plugins();
# In theory we just kill our children and they're
# automatically respawned by the various child
# death handlers.
$self->kill_children();
}
sub create_cache {
my( $self ) = @_;
eval { require Cache::FastMmap; }
and return Cache::FastMmap->new( share_file => "var/cache-fastmmap", init_file => 1 );
eval { require Cache::Mmap; }
and return Cache::Mmap->new( "var/cache-mmap", { buckets => 89, bucketsize => 64 * 1024 } );
eval { require Cache::File; }
and return Cache::File->new( cache_root => 'var/cache-file', default_expires => '6000 sec' );
die "Failed to properly create a cache object! Please install Cache::FastMmap, Cache::Mmap or Cache::File\n";
}
#---------------------
# Loading methods
#---------------------
sub _load_plugins {
my( $self ) = @_;
my $plugin_dir = $self->{main_conf}->{plugin_dir} || 'plugins';
opendir my $dh, $plugin_dir or die "Failed to open plugin dir: $plugin_dir: $!\n";
while( defined( my $file = readdir $dh ) ) {
next unless $file =~ /\.pm$/;
local $@;
local *DATA; # Prevent previous file's __DATA__
# sections being read for this new file.
my $plugin_return = do "$plugin_dir/$file";
if( not $plugin_return or $@ ) {
error "Failed to load plugin: $plugin_dir/$file $@\n";
next;
}
(my $name = $file) =~ s/\.pm$//;
my $plugin_obj;
my $help_text;
if( ref $plugin_return eq 'CODE' ) {
$plugin_obj = Bot::BB3::PluginWrapper->new( $name, $plugin_return );
$help_text = join '', <DATA>;
}
elsif( ref $plugin_return eq '' ) { #String representing package name, I hope
local $@;
eval {
$plugin_obj = $plugin_return->new();
# Fo' Realz.
# strict won't let me abuse typeglob symbolic refs properly!
no strict;
if( *{"${plugin_return}::DATA"}{IO} ) {
$help_text = join '', readline *{"${plugin_return}::DATA"};
}
};
if( not $plugin_obj or $@ ) {
warn "Failed to instantiate $plugin_return from $plugin_dir/$file $@\n";
next;
}
}
if( not $plugin_obj ) {
warn "Failed to get a plugin_obj from $plugin_dir/$file for unknown reasons $plugin_return.\n";
next;
}
$plugin_obj->{help_text} = $help_text;
push @{ $self->{plugins} }, $plugin_obj;
}
$self->_pre_build_plugin_chain();
$self->_pre_load_default_plugin();
for my $plugin ( @{ $self->{plugins} } ) {
local $@;
$plugin->can("postload") and
eval { $plugin->postload($self) };
if( $@ ) { warn "$plugin->{name}: postload failed: $@"; }
}
return scalar @{ $self->{plugins} };
}
sub _pre_build_plugin_chain {
my( $self ) = @_;
my $plugins = $self->{plugins};
my( $pre,$post,$commands,$handlers );
for my $plugin ( @$plugins ) {
my $opts = $plugin->{opts};
if( $opts->{pre_process} ) {
push @$pre, $plugin;
}
if( $opts->{post_process} ) {
push @$post, $plugin;
}
if( $opts->{command} ) {
$commands->{ $plugin->{name} } = $plugin;
if( $plugin->{aliases} ) {
$commands->{ $_ } = $plugin
for @{ $plugin->{aliases} };
}
}
if( $opts->{handler} ) {
push @$handlers, $plugin;
}
}
$self->{plugin_chain} = {
pre_process => $pre,
post_process => $post,
commands => $commands,
handlers => $handlers
};
}
sub _pre_load_default_plugin {
my( $self ) = @_;
if( my $default = $self->{main_conf}->{plugin_manager}->{default_plugin} ) {
if( not ref $default ) { $default = [$default] } # Clean up Config::General randomness
my @default_chain;
for( @$default ) {
my @plugins = split " ", $_; #I'm not really sure what the format is. Also, magic split.
for( @plugins ) {
my $plugin = $self->get_plugin( $_ );
if( $plugin ) { push @default_chain, $plugin }
}
}
$self->{default_plugin_chain} = \@default_chain;
}
else {
$self->{default_plugin_chain} = [];
}
}
#--------------------------------
# Executed inside the forked child
#--------------------------------
sub _start_plugin_child {
my( $self ) = @_;
srand; # We deliberately call srand since when we fork all children will have the same initial seed
#POE::Kernel->run; # Attempt to suppress the warning about ->run never being called.
for( @{ $self->get_plugins } ) {
if( $_->can('initialize') ) {
local $@;
eval { $_->initialize($self, $self->{child_cache}) };
if( $@ ) { warn "$_->{name} failed to initialize: $@"; }
}
}
my $filter = POE::Filter::Reference->new;
binmode( STDIN ); binmode( STDOUT );
my $handled_counter = 0;
while( 1 ) {
my $stream;
sysread STDIN, $stream, 4096
or die "Child $$ failed to read: $!\n";
my $filter_refs = $filter->get( [$stream] );
for my $said ( @$filter_refs ) {
$handled_counter++;
#-----
# Execute chain
#-----
my $chain = $self->_create_plugin_chain( $said );
# Only add the default if we're being addressed
if( $said->{addressed} ) {
push @{ $chain->[1] }, @{ $self->{default_plugin_chain} }; # Append default plugins to the command section
# of the plugin chain
}
my $results = $self->_execute_plugin_chain( $said, $chain );
warn "Got some output: [$results]\n";
if( $results !~ /\S/ and $said->{addressed} ) {
#$results = "Couldn't match input.";
}
#----
# Output
#----
#if( length $results and $results =~ /\S/) {
# Always output something so the handler knows we're done.
for( @{$filter->put([ [$said, $results] ])} ) {
syswrite STDOUT, $_;
}
#}
}
if( $handled_counter > $self->{main_conf}->{child_handle_count} ) {
last;
}
}
warn "$$: Fell off the end, exiting\n";
# Exit the child
exit;
}
sub _create_plugin_chain {
my( $self, $said ) = @_;
my $pre_built_chains = $self->{plugin_chain};
my( $pre, $post ) = @{$pre_built_chains}{ qw/pre_process post_process/ };
my $handlers = $self->_filter_plugin_list( $said, $pre_built_chains->{ handlers } );
;
#---
# Parse said/commands
#---
my $commands = $pre_built_chains->{commands};
my $command_list = $self->_parse_for_commands( $said, $commands );
return [ $pre, $command_list, $handlers, $post ];
}
sub _parse_for_commands {
my( $self, $said, $commands ) = @_;
my $command_re = join '|', map "\Q$_", keys %$commands;
$command_re = qr/$command_re/; #TODO move to _pre_build_chains and switch to Trie
if( $said->{body} =~ s/^\s*($command_re)[ \t:,;.-]\s*(.+)/$2/
or $said->{body} =~ s/^\s*($command_re)\s*$// ) {
my $found_command = $1;
my $args = $2;
my $command = $commands->{ $found_command };
warn "found $found_command - $args\n";
# takes and returns array ref
my $filter_check = $self->_filter_plugin_list( $said, [$command] );
if( @$filter_check ) { # So check if the one argument passed
# Return an array ref..
$said->{recommended_args} = [ split /\s+/, $args ];
$said->{command_match} = $found_command;
return $filter_check;
}
}
return [];
}
sub _filter_plugin_list {
my( $self, $said, $plugins ) = @_;
my @chain;
for( @$plugins ) {
my $conf = $self->plugin_conf( $_->{name}, $said->{server}, $said->{channel} );
# Explicitly skip addressed checks for special channels
if( $said->{channel} !~ /^\*/ ) {
next if $conf->{addressed} and not $said->{addressed};
}
next if $conf->{access} eq 'op' and not ( $said->{by_chan_op} or $said->{by_root} );
next if $conf->{access} eq 'root' and not $said->{by_root};
push @chain, $_;
}
return \@chain;
}
sub _execute_plugin_chain {
my( $self, $said, $chain ) = @_;
my( $pre, $commands, $handlers, $post ) = @$chain;
for( @$pre ) {
$_->pre_process( $said, $self );
}
my $total_output = [];
for( @$commands ) {
local $@;
my( $return, $output ) = eval { $_->command( $said, $self ) };
if( $@ ) { push @$total_output, "Error: $@"; next; }
warn "$_->{name} - $return - $output\n";
push @$total_output, $output;
if( $return eq 'handled' ) {
last;
}
}
for( @$handlers ) {
local $@;
my( $output ) = eval { $_->handle( $said, $self ) };
if( $@ ) { push @$total_output, "Error: $@"; next; }
push @$total_output, $output;
}
my $output = join " ", @$total_output;
for( @$post ) {
$_->post_process( $said, $self, \$output );
}
return $output;
}
#--------------------------------
# Note, really queues the event.
# Should definitely only be called by
# external users at this point..
sub execute_said {
my( $self, $kernel, $sender, $said ) = @_[OBJECT,KERNEL,SENDER,ARG0];
$said->{parent_session} = $sender->ID unless $said->{parent_session};
warn "Queuing said.. $said->{body}\n";
push @{ $self->{said_queue} }, $said;
$self->yield( 'handle_said_queue' );
}
sub handle_said_queue {
my( $self, $kernel ) = @_[OBJECT,KERNEL];
my $queue = $self->{said_queue};
my $children = [ values %{ $self->{children} } ];
return unless $queue and @$queue;
while( defined( my $said = shift @$queue ) ) {
warn "Queuing $said\n";
for( @$children ) {
warn "Checking ", $_->{wheel}->PID, ": $_->{queue}";
if( not $_->{queue} ) {
$_->{queue} = $said;
$_->{wheel}->put( $said );
$said->{attempts}++;
warn "Queueing $said for ", $_->{wheel}->PID;
last;
}
}
}
if( $queue and @$queue ) {
$kernel->delay( handle_said_queue => 2 );
}
}
# Helper method!
sub _spawn_child {
my( $self ) = @_;
my $child = POE::Wheel::Run->new(
Program => sub { $self->_start_plugin_child; },
NoSetSid => 1, #Ensure that SIGINTS to the main process kill our children
StdioFilter => POE::Filter::Reference->new,
StderrFilter => POE::Filter::Line->new,
StdinEvent => 'child_flushed',
StdoutEvent => 'child_output',
StderrEvent => 'child_err',
ErrorEvent => 'child_fail',
CloseEvent => 'child_close',
);
#push @{ $self->{children} }, $child;
my $child_struct = {
wheel => $child,
graceful_shutdown => 0,
queue => 0,
};
$self->{children}->{ $child->ID } = $child_struct;
$self->{children_by_pid}->{ $child->PID } = $child->ID;
warn "Created child: ", $child->ID;
$poe_kernel->sig_child( $child->PID, 'child_die' );
}
# This is probably called multiple times every time a child dies
# so we need to make sure it gracefully handles all of the possible
# cases.
sub child_die {
my( $self, $pid ) = @_[OBJECT,ARG1];
my $id = delete $self->{children_by_pid}->{$pid};
return unless $id;
# Delete child operation
warn "Deleting: $id";
my $child = delete $self->{children}->{$id};
return unless $child;
# If the dead child had a queue ready then we requeue to make sure it
# gets handled eventually.
# If the $said has been tried a couple of times it's probably causing
# the child to die some how, so we skip it.
if( $child->{queue} and $child->{queue}->{attempts} < 2 ) {
#TODO methodize! (unshift and handle)
unshift @{ $self->{said_queue} }, $child->{queue};
$self->yield( 'handle_said_queue' );
}
# Go go gadget reproduction.
$self->_spawn_child;
}
#---------------------------------------------
# Getters
#---------------------------------------------
sub get_children {
return values %{ $_[0]->{children} };
}
#---------------------------------------------
sub _start {
my( $self, $kernel ) = @_[OBJECT,KERNEL];
my $start_child_count = $self->{main_conf}->{start_plugin_children};
for( 1 .. $start_child_count ) {
$self->_spawn_child();
}
$kernel->delay_set( 'adjust_child_count', 5 );
}
sub adjust_child_count {
my( $self, $kernel ) = @_[OBJECT,KERNEL];
#for( @w
}
sub child_flushed {
my( $self, $kernel, $child_id ) = @_[OBJECT,KERNEL,ARG0];
my $child = $self->{children}->{$child_id};
$child->{flush_time} = time;
$kernel->delay_set( child_time_limit => 25, $child_id, $child->{queue} );
warn "Flushed to child $child_id\n";
}
sub child_output {
my( $self, $kernel, $output, $child_id ) = @_[OBJECT,KERNEL,ARG0,ARG1];
my( $said, $text ) = @$output;
warn "Got some child output! $text\n";
my $child = $self->{children}->{$child_id};
warn "Deleting child queue: $child_id, $child->{queue}";
$child->{queue} = undef;
$self->yield('handle_said_queue');
#TODO turn into a method (respond to parent)
my $parent = $said->{parent_session};
if( my $commands = delete $said->{special_commands} ) {
for( @$commands ) {
my $name = shift @$_;
if( $name =~ s/^pci_// ) {
$kernel->post( $parent => handle_special_commands => $said, $name, @$_ );
}
elsif( $name =~ s/^pm_// ) {
$self->$name( @$_ );
}
elsif( $name =~ s/^bb3_// ) {
$self->{bb3}->$name( @$_ );
}
}
}
#if( length $text and $text =~ /\S/) {
# Always post back to our user.
$kernel->post( $parent => 'plugin_output', $said, $text );
#}
}
sub child_err {
my( $self, $err_output, $child_id ) = @_[OBJECT,ARG0,ARG1];
return unless $err_output =~ /\S/;
warn "\n\tChild $child_id: $err_output\n";
}
sub child_fail {
my( $self, $op, $err_num, $err_str, $child_id ) = @_[OBJECT,ARG0,ARG1,ARG2,ARG3];
my $child = $self->{children}->{$child_id};
return unless $child;
warn "Crap, our child $child_id failed: $op $err_num $err_str\n";
$self->yield( child_die => $child->{wheel}->PID );
}
sub child_close {
my( $self, $child_id ) = @_[OBJECT,ARG0];
my $child = $self->{children}->{$child_id};
return unless $child;
warn "Child $child_id closed\n";
$self->yield( child_die => $child->{wheel}->PID );
}
sub child_time_limit {
my( $self, $kernel, $child_id, $queue_ref ) = @_[OBJECT,KERNEL,ARG0,ARG1];
my $child = $self->{children}->{$child_id};
warn "Checking the time limit on $child_id";
if( $child and $child->{queue} == $queue_ref ) {
if( time() - $child->{flush_time} > 20 ) {
warn "Killing $child_id because $child->{flush_time} is more than 20 ago from ", time();
kill 9, $child->{wheel}->PID;
}
else {
$kernel->delay_set( child_time_limit => 10, $child_id, $queue_ref );
}
}
# If they don't match, child has already responded and we can ignore it.
}
sub please_die {
my( $self, $kernel ) = @_[OBJECT,KERNEL];
$self->kill_children();
}
#-------------------
# Slightly less random cruft.
# Used by plugins. Move to common
# plugin base class?
#-------------------
sub create_table {
my( $self, $dbh, $table_name, $create_table_sql ) = @_;
local $@;
eval {
$dbh->do("SELECT * FROM $table_name LIMIT 1");
};
if( $@ =~ /no such table/ ) {
# Race Conditions could cause two threads to create this table.
local $@;
eval {
$dbh->do( $create_table_sql );
};
# Stupid threading issues.
# All of the children try to do this at the same time.
# Suppress most warnings.
if( $@ and $@ !~ /already exists/ and $@ !~ /database schema has changed/ ) {
error "Failed to create table: $@\n";
}
#Success!
}
elsif( $@ ) {
error "Failed to access dbh to test table: $@";
warn "Caller: ", join " ", map "[$_]", caller;
}
}
#-------------------------------------------
# Random cruft
# Should probably be moved somewhere.
#-------------------------------------------
{
my %plugin_conf_cache;
sub plugin_conf
{
my( $self, $command, $server, $channel ) = @_;
my $plugin_conf = $self->{plugin_conf};
if( local $_ = $plugin_conf_cache{$server}->{$channel}->{$command} ) {
return $_;
}
my $opts = {};
for( @$plugin_conf )
{
my $glob = $_->[1];
if( match_glob( lc $glob, lc $server ) )
{
for( @{ $_->[2] } )
{
if( match_glob( lc $_->[1], lc $channel ) )
{
for( @{ $_->[2] } )
{
if( match_glob( lc $_->[1], lc $command ) )
{
my $new_opts = $_->[2];
$opts = { %$opts, %$new_opts };
}
}
}
}
}
}
# Convert 'false' type strings into perl false values.
for( keys %$opts )
{
my $v = $opts->{$_};
if( $v eq 'false' or $v eq 'null' or $v eq 'off' or $v eq 0 )
{
$opts->{$_} = undef;
}
}
$plugin_conf_cache{$server}->{$channel}->{$command} = $opts;
return $opts;
}
}
1;

View file

@ -1,56 +0,0 @@
package Bot::BB3::PluginWrapper;
use strict;
{
package PluginWrapper::WrapSTDOUT;
sub TIEHANDLE {
my( $class, $buffer_ref ) = @_;
return bless { buffer => $buffer_ref }, $class;
}
sub PRINT {
my( $self, @args ) = @_;
${ $self->{buffer} } .= join $", @args;
return 1;
}
sub PRINTF {
my( $self, $format, @args ) = @_;
${ $self->{buffer} } .= sprintf $format, @args;
return 1;
}
}
sub new {
my( $class, $name, $coderef ) = @_;
my $self = bless { coderef => $coderef, name => $name }, $class;
$self->{opts} = {
command => 1,
};
return $self;
}
sub command {
my( $self, $said, $pm ) = @_;
my( $name ) = $self->{name};
my $output;
local *STDOUT;
tie *STDOUT, 'PluginWrapper::WrapSTDOUT', \$output;
$self->{coderef}->($said,$pm);
untie *STDOUT;
return( 'handled', $output );
}
1;

View file

@ -1,198 +0,0 @@
10
dir
475
svn://erxz.com/bb3/branches/perlbuut/lib/Bot/BB3/Roles
svn://erxz.com/bb3
2009-10-03T22:53:42.528878Z
475
simcop
dcb1cea6-7f7e-4c78-8a22-148ace8ce36e
SocketMessageIRC.pm
file
2009-10-03T22:54:20.000000Z
10275c1032b5d50d7e178744819a469a
2009-10-03T22:53:42.528878Z
475
simcop
1666
IRC.pm
file
476
2009-10-03T22:57:00.000000Z
ecb2d397d28a39c350728a2f504a70b3
2009-10-03T22:56:07.617451Z
476
simcop
has-props
14353
Web.pm
file
476
2009-10-03T22:57:00.000000Z
669673837edc7d1a4285a93495e80b33
2009-10-03T22:56:07.617451Z
476
simcop
has-props
3341
PasteBot.pm
file
2009-10-03T22:54:20.000000Z
0aac2e184b8116ec32a22f354ff7876b
2009-10-03T22:53:42.528878Z
475
simcop
7515
Console.pm
file
476
2009-10-03T22:57:00.000000Z
38ec47f6cca221c35dbbc31c2960891e
2009-10-03T22:56:07.617451Z
476
simcop
has-props
2857

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,142 +0,0 @@
package Bot::BB3::Roles::Console;
use POE;
use POE::Session;
use POE::Wheel::SocketFactory;
use strict;
use Bot::BB3::Logger;
sub new {
my( $class, $conf, $pm ) = @_;
my $self = bless { conf => $conf, pm => $pm }, $class;
$self->spawn_session;
return $self;
}
sub spawn_session {
my( $self ) = @_;
$self->{session} = POE::Session->create(
object_states => [
$self => [
qw/_start
socket_new factory_fail
socket_read socket_write plugin_output/
]
]
);
return $self;
}
sub _start {
my( $self ) = @_[OBJECT];
$self->{socket_factory} = POE::Wheel::SocketFactory->new(
BindAddress => "127.0.0.1",
BindPort => $self->{conf}->{roles}->{console}->{port} || 10041,
SuccessEvent => 'socket_new',
FailureEvent => 'factory_fail',
Reuse => 'on',
);
}
sub socket_new {
my( $self, $handle ) = @_[OBJECT,ARG0];
warn "Got a socket: $handle\n";
my $wheel = POE::Wheel::ReadWrite->new(
Handle => $handle,
Driver => POE::Driver::SysRW->new,
InputFilter => POE::Filter::Line->new,
OutputFilter => POE::Filter::Stream->new,
InputEvent => 'socket_read',
FlushedEvent => 'socket_write',
ErrorEvent => 'socket_error',
);
$self->{wheels}->{$wheel->ID} = $wheel;
}
sub socket_error {
my( $self, $op, $errstr, $errnum, $id ) = @_[OBJECT,ARG0..ARG3];
#TODO figure out which errors we don't care about.
warn "Socket Error: $op - $errstr - $errnum\n";
delete $self->{wheels}->{$id};
}
sub factory_fail {
my( $self, $op, $errstr, $errnum, $id ) = @_[OBJECT,ARG0..ARG3];
warn "Help, I'm falling! $op - $errstr $errnum";
#Attempt a respawn
delete $self->{session};
# TODO look for ways to stop a session?
$self->spawn_session;
}
sub socket_write {
my( $self, $id ) = @_[OBJECT,ARG0];
warn "Written some data to $id\n";
}
sub socket_read {
my( $self, $input, $id ) = @_[OBJECT,ARG0,ARG1];
#$input is a line containing a command
s/^\s+//,s/\s+$// for $input;
my( $command, @args ) = split /\s+/, $input;
# Command disabled since we don't have access
# to that object any more.
my %special_commands = (
#'list_pcis' => sub {
#my $ic=$self->{parent}->{irc_components};
#for( keys %$ic ) {
#$self->{wheels}->{$id}->put( "$_: " . $ic->{$_}->server_name . "\n" );
#}
#},
);
if( exists $special_commands{$command} ) {
$special_commands{$command}->();
}
else {
my $said = {
body => $input,
raw_body => $input,
my_name => 'CommandConsole',
addressed => 1,
recommended_args => \@args,
channel => '*special',
name => 'CC',
ircname => 'CC',
host => '*special',
server => '*special',
pci_id => $id,
};
$self->{pm}->yield( execute_said => $said );
}
}
sub plugin_output {
my( $self, $said, $output ) = @_[OBJECT,ARG0,ARG1];
my $wheel_id = $said->{pci_id};
$self->{wheels}->{$wheel_id}->put( "$output\n" );
}
1;

View file

@ -1,546 +0,0 @@
package Bot::BB3::Roles::IRC;
use Bot::BB3::Logger;
use POE;
use POE::Session;
use POE::Wheel::Run;
use POE::Filter::Reference;
use POE::Component::IRC::Common qw/parse_user l_irc/;
use POE::Component::IRC::State;
use POE::Component::IRC::Plugin::AutoJoin;
use POE::Component::IRC::Plugin::Connector;
use POE::Component::IRC::Plugin::NickReclaim;
use Memoize qw/memoize/;
use Data::Dumper;
use Socket;
use utf8;
use strict;
sub new {
my( $class, $conf, $plugin_manager ) = @_;
my $self = bless { conf => $conf, pm => $plugin_manager }, $class;
my $bots = $self->{conf}->{bot};
warn Dumper $bots;
for( @$bots ) {
warn "Spawning Bot: ", Dumper $_;
# Blah blah evil
my $ip = `/sbin/ifconfig | perl -nle' if( /inet addr:(\\d+\\.\\d+\\.\\d+\\.\\d+)/ ) { print \$1; exit }'`;
# This is to fix a bug with dcc not recognizing our ip..
my $poco_irc = POE::Component::IRC::State->spawn(
nick => $_->{nick} || $_->{botname},
username => $_->{username} || $_->{nick} || $_->{botname},
$_->{password} ? (password => $_->{password}) : (),
server => $_->{server},
port => $_->{port} || 6667,
ircname => $_->{ircname} || $_->{nick} || $_->{botname},
);
# Fixes a bug where our remote IP was being sent as 0.0.0.0
# TODO remove obviously, but maybe add a configuration option to do this.
# set to 'dynamic' or a host/ip name.
$poco_irc->{dcc}->nataddr($ip); #Hideously violate encapsulation because I think we need to..
$poco_irc->plugin_add(
AutoJoin => POE::Component::IRC::Plugin::AutoJoin->new(
Channels => $_->{channel}
)
);
$poco_irc->plugin_add( Connector => POE::Component::IRC::Plugin::Connector->new );
$poco_irc->plugin_add( Reclaim => POE::Component::IRC::Plugin::NickReclaim->new( poll => 120 ) );
my $pci_id = $poco_irc->session_id;
$self->{bot_confs}->{ $pci_id } = $_;
$self->{irc_components}->{ $pci_id } = $poco_irc;
$self->_build_ignore_hash( $pci_id, $_ );
}
$self->{session} = POE::Session->create(
object_states => [
$self => [ qw/
_start
irc_001
irc_public
irc_ctcp_action
irc_join
irc_invite
irc_msg
irc_registered
irc_474
irc_dcc_request
irc_dcc_start
irc_dcc_chat
irc_ctcp_chat
plugin_output
handle_special_commands
external_message
clear_dynamic_ignore
channel_list
stop_talking
start_talking
comfuckpong
/
]
],
);
return $self;
}
sub _build_ignore_hash {
my( $self, $pci_id, $pci_conf ) = @_;
for( @{ $pci_conf->{ignore} } ) {
$self->{bot_ignores}->{$pci_id}->{l_irc $_} = $pci_id;
}
}
#------------------------------------------------------------------------------
# PUBLIC METHODS
#------------------------------------------------------------------------------
sub comfuckpong
{
my ($sender, $kernel, $heap) = @_[SENDER, KERNEL, HEAP];
my $d = $heap->{irc}->server_name();
$heap->{irc}->yield( quote => "PONG $d\n");
$kernel->delay_add(comfuckpong => 50);
}
sub get_bot_conf {
my( $self, $poco_irc ) = @_;
my $id = ( ref $poco_irc ) ? $poco_irc->session_id : $poco_irc;
return $self->{bot_confs}->{ $id };
}
sub get_aliases {
my( $self, $pci ) = @_;
my $conf = $self->get_bot_conf( $pci );
my @alias_return;
my $aliases = $conf->{alias};
if( not ref $aliases ) { $aliases = [ $aliases ]; }
my $aliase_res = $conf->{alias_re};
if( not ref $aliase_res ) { $aliase_res = [ $aliase_res ] }
return [ grep defined, @$aliase_res, map "\Q$_", grep defined, @$aliases ];
}
memoize( 'get_aliases' );
sub get_component {
my( $self, $pci_id ) = @_;
return $self->{irc_components}->{ $pci_id };
}
sub is_ignored {
my( $self, $said ) = @_;
my $lc_nick = l_irc $said->{name};
my $lc_body = l_irc $said->{body};
if( exists $self->{bot_ignores}->{$said->{pci_id}}->{$lc_nick} ) {
return 1;
}
my $msg_queue = $self->{dynamic_ignores}->{$said->{pci_id}}->{$lc_nick} ||= [];
push @$msg_queue, $lc_body;
$poe_kernel->delay_set( clear_dynamic_ignore => 10, $said->{pci_id}, $lc_nick );
my $match_count;
for( @$msg_queue ) {
if( $_ eq $lc_body ) {
if( ++$match_count > 4 ) {
return 1;
}
}
}
return;
}
sub dispatch_said {
my( $self, $said ) = @_;
use Data::Dumper;
warn "DISPATCH_SAID $said->{pci_id} = $said->{channel}\n";
warn Dumper $self->{squelched_channels};
if( $self->{squelched_channels}->{$said->{pci_id}}->{lc $said->{channel}}
and not $said->{addressed}
) {
return;
}
warn "Sending on execute_said\n";
$self->{pm}->yield( execute_said => $said );
}
#------------------------------------------------------------------------------
# POE STATES
#------------------------------------------------------------------------------
sub _start {
my( $self, $kernel, $session ) = @_[OBJECT,KERNEL,SESSION];
$kernel->signal( $kernel, 'POCOIRC_REGISTER', $session->ID, 'all' );
$kernel->alias_set( __PACKAGE__ );
}
sub stop_talking {
my( $self, $poco_id, $channel ) = @_[OBJECT,ARG0,ARG1];
warn "RECEIVED STOP TALKING: $poco_id, $channel\n";
$self->{squelched_channels}->{$poco_id}->{lc $channel} = 1;
}
sub start_talking {
my( $self, $poco_id, $channel ) = @_[OBJECT,ARG0,ARG1];
delete $self->{squelched_channels}->{$poco_id}->{lc $channel};
}
sub irc_registered {
my( $self, $sender, $kernel, $pci ) = @_[OBJECT,SENDER,KERNEL,ARG0];
$pci->yield( connect => {} );
}
sub _said {
my( $self, $sender, $kernel ) = @_[OBJECT,SENDER,KERNEL];
my $caller = ((caller(1))[3]);
$caller =~ /:([^:]+)$/ and $caller = $1;
my $pci = $self->get_component($sender->ID);
my $said = {};
$said->{server} = $pci->server_name;
$said->{my_name} = $pci->nick_name;
$said->{pci_id} = $pci->session_id;
#--------------------------
# Method Specific Logic
#--------------------------
if( $caller eq 'irc_public' ) {
$said->{ sender_raw } = $_[ARG0];
$said->{ body_raw } = $_[ARG2];
$said->{ channel } = $_[ARG1]->[0];
}
elsif( $caller eq 'irc_msg' ) {
$said->{ sender_raw } = $_[ARG0];
$said->{ body_raw } = $_[ARG2];
$said->{ channel } = '*irc_msg';
$said->{ addressed } = 1;
}
elsif( $caller eq 'irc_ctcp_action' ) {
$said->{ sender_raw } = $_[ARG0];
$said->{ body_raw } = $_[ARG2];
$said->{ channel } = $_[ARG1]->[0];
}
elsif( $caller eq 'irc_dcc_chat' ) {
$said->{ body_raw } = $_[ARG3];
$said->{ channel } = '*dcc_chat';
$said->{ addressed } = 1;
# We only get the IP Address from the dcc_chat events so we need to try to
# turn it back in to a hostname, since that's usually what we have here
# Presumably the irc server is normally doing a rdns lookup anyway
# which is what we're trying to emulate here.
# In this case we pack the IP address and an arbitrary port (80) in to a
# magically opaque struct and then unpack it back in to .. something
# using sockaddr_in and then we can get the hostname from gethostbyaddr
my $addr_struct = pack_sockaddr_in( 80, inet_aton($_[ARG4]) );
my($port,$iaddr)=sockaddr_in($addr_struct);
$said->{ host } = gethostbyaddr($iaddr,AF_INET());
# Recreate the sender_raw in the form of nick!nick@hostname so our root check
# later on will work properly
$said->{ sender_raw } = $_[ARG1] . '!' . $_[ARG1] . '@' . $said->{host};
}
else {
die "ERROR, _said called by unknown caller: $caller";
}
#--------------------------
my @user_info = parse_user( $said->{ sender_raw } );
for( qw/name ircname host/ ) {
if( not defined $user_info[0] ) {
last;
}
$said->{$_} = shift @user_info;
}
#--------------------------
# Check for our own name
#--------------------------
$said->{body} = $said->{body_raw};
if( $said->{my_name} ) { #TODO verify that we need this if check.
my $body = $said->{body_raw};
my $aliases = $self->get_aliases( $pci );
my $name_re = "(?:" . join( "|", map "(?:$_)", $said->{my_name}, @$aliases ) . ")";
if( $body =~ s/^\s*($name_re)\b\s*[;:,. \t-]?\s*// ) {
$said->{body} = $body;
$said->{addressed} = 1;
$said->{addressed_as} = $1;
}
}
#--------------------------
#--------------------------
# Permission Checks
#--------------------------
my $conf = $self->get_bot_conf( $pci );
my $root_mask = $conf->{root_mask};
$said->{by_root} = ( $said->{ sender_raw } =~ $root_mask );
$said->{by_chan_op} = $pci->is_channel_operator( $said->{channel}, $said->{name} );
warn Data::Dumper->Dump([[$pci->nick_channels($said->{name})]], ["NICK_CHANS"]);
$said->{in_my_chan} = ($pci->nick_channels($said->{name})) ? 1 : 0;
return $said;
}
sub irc_public {
my( $self ) = @_[OBJECT];
my $said = _said( @_ );
if( $self->is_ignored( $said ) ) {
warn "Ignoring $said->{name}\n";
return;
}
warn "Yielding to execute_said\n";
warn Dumper $said;
$self->dispatch_said( $said );
}
sub irc_msg {
my( $self ) = @_[OBJECT];
my $said = _said( @_ );
return if $self->is_ignored( $said );
$self->dispatch_said( $said );
}
sub irc_ctcp_action {
my( $self ) = @_[OBJECT];
my $said = _said( @_ );
}
sub irc_join {
my( $self ) = @_[OBJECT];
}
sub irc_invite {
my( $self, $kernel, $sender, $inviter, $channel ) = @_[OBJECT,KERNEL,SENDER,ARG0,ARG1];
$kernel->post( $sender, join => $channel );
}
# Naturally this is called after we've successfully
# connected to an irc server so we queue up some
# channel joins and so forth.
sub irc_001 {
my( $self, $kernel, $sender ) = @_[OBJECT,KERNEL,SENDER];
my $bot_conf = $self->get_bot_conf( $sender->ID );
my $channels = $bot_conf->{channel};
# GIANT HACK
if( $bot_conf->{server} =~ /freenode/ ) {
open my $fh, "/home/buu/nickservpass" or goto HACKEND; #sorry
my $pass = <$fh>;
chomp $pass;
$kernel->post( $sender, privmsg => 'nickserv', "identify $pass" );
}
HACKEND:
# END HACK
$kernel->delay_add(comfuck=>50);
# May be an array ref.
for( ref $channels ? @$channels : $channels ) {
$kernel->post( $sender, join => $_ );
}
}
sub irc_474 {
my( $self, @args ) = @_[OBJECT,ARG0..$#_];
warn "Error, banned from channel: @args\n";
}
# Triggered by a delay_set whenever a line is added to dynamic_ignores
sub clear_dynamic_ignore {
my( $self, $pci_id, $nick ) = @_[OBJECT,ARG0,ARG1];
shift @{$self->{dynamic_ignores}->{$pci_id}->{$nick}};
}
sub irc_ctcp_chat {
my( $self, $sender, $user, $target ) = @_[OBJECT,SENDER,ARG0,ARG1];
my $pci = $self->get_component( $sender->ID );
warn "Matching: ", $pci->nick_name, " against $target->[0]\n";
if( l_irc($pci->nick_name) eq l_irc($target->[0]) ) {
$pci->yield( dcc => (parse_user $user)[0], 'CHAT' );
}
}
sub irc_dcc_request {
my( $self, $sender, $user, $type, $cookie ) = @_[OBJECT,SENDER,ARG0,ARG1,ARG3];
my $pci = $self->get_component( $sender->ID );
if( lc($type) eq 'chat' ) {
$pci->yield( dcc_accept => $cookie );
}
}
# Should always be chat events at the moment..
sub irc_dcc_start {
my( $self, $sender, $cookie, $nick ) = @_[OBJECT,SENDER,ARG0,ARG1];
my $welcome = <<'WELCOME';
____ ____ __ _____ ____
/ __ )__ ____ __/ __ )____ / /_ _ _|__ / / __ \
/ __ / / / / / / / __ / __ \/ __/ | | / //_ < / / / /
/ /_/ / /_/ / /_/ / /_/ / /_/ / /_ | |/ /__/ // /_/ /
/_____/\__,_/\__,_/_____/\____/\__/ |___/____(_)____/
WELCOME
$welcome .= "Hello $nick. Welcome to BuuBot's dcc chat.\nAll plugins are available, try 'plugins' and 'help plugins' for a list.";
$poe_kernel->post( $sender => dcc_chat => $cookie, $welcome );
}
sub irc_dcc_chat {
my( $self, $sender, $cookie, $nick, $text ) = @_[OBJECT,SENDER,ARG0,ARG1,ARG3];
my $pci = $self->get_component( $sender->ID );
my $said = _said( @_ );
$said->{dcc_id} = $cookie;
return if $self->is_ignored( $said );
warn "================================== HOST $_[ARG4] =========================\n";
use Data::Dumper;
warn Dumper $said;
$self->dispatch_said( $said );
}
#-----------------------------------------------------------------------------
# PUBLIC POE API
#-----------------------------------------------------------------------------
sub external_message {
my( $self, $server, $nick, $channel, $message ) = @_[OBJECT,ARG0,ARG1,ARG2,ARG3];
warn "Received external message, $server, $nick, $channel, $message\n";
for my $pci_id ( keys %{ $self->{bot_confs} } ) {
my $conf = $self->{bot_confs}->{$pci_id};
my $poco_irc = $self->get_component($pci_id);
if( $conf->{server} eq $server
and ( $conf->{nick} eq $nick or $conf->{botname} eq $nick )
and exists $poco_irc->channels()->{$channel}
) {
warn "Sending private message: $pci_id, $channel, $message\n";
$self->get_component($pci_id)->yield( privmsg => $channel => $message );
}
}
}
sub channel_list {
my( $self, $kernel, $sender ) = @_[OBJECT,KERNEL,SENDER];
my $channels;
for( keys %{ $self->{irc_components} } ) {
my $poco_irc = $self->{irc_components}->{$_};
my $poco_conf = $self->{bot_confs}->{$_};
$channels->{ $poco_conf->{server} }
->{ $poco_conf->{nick} || $poco_conf->{botname} }
= [ keys %{ $poco_irc->channels } ];
}
return $channels;
}
sub plugin_output {
my( $self, $said, $text ) = @_[OBJECT,ARG0,ARG1];
utf8::decode( $text );
return unless $text =~ /\S/;
$text =~ s/\0/\\0/g; # Replace nulls to prevent them truncating strings we attempt to output.
my $pci = $self->get_component( $said->{pci_id} );
# sub send_text( $said, $text ) !
if( $said->{channel} eq '*irc_msg' ) {
my $messages_sent = 0;
MESSAGES: for my $text ( split /\r?\n/, $text ) {
# Send multiple messages if we're talking in a private chat
# Note that in the future we'll probably want to generalize channels
# that receive multiple lines and those that don't..
while( length $text ) {
my $substr = substr( $text, 0, 400, '' );
$pci->yield( privmsg => $said->{name} => $substr );
# Try to avoid sending too many lines, since it may be annoying
# and it tends to prevent the bot from sending other messages.
last MESSAGES if $messages_sent++ > 5;
}
}
}
elsif ( $said->{channel} eq '*dcc_chat' ) {
$pci->yield( dcc_chat => $said->{dcc_id} => $text );
}
else {
$text =~ s/\r?\n/ /g;
$pci->yield( privmsg => $said->{channel} => "$said->{name}: $text" );
}
}
sub handle_special_commands {
my( $self, $kernel, $said, @command ) = @_[OBJECT,KERNEL,ARG0,ARG1..$#_];
my $pci = $self->get_component($said->{pci_id});
$pci->yield( @command );
}
1;

View file

@ -1,337 +0,0 @@
package Bot::BB3::Roles::PasteBot;
use POE;
use POE::Component::Server::SimpleHTTP;
use HTTP::Status;
use CGI;
use Template;
use strict;
our( $INDEX_HTML, $RECEIVED_PASTE_HTML, $DISPLAY_PASTE_HTML );
sub new {
my( $class, $conf, $pm ) = @_;
my $self = bless { conf => $conf, pm => $pm }, $class;
$self->{hostname} = $conf->{roles}->{pastebot}->{hostname} || "127.0.0.1";
$self->{hostname} =~ s{^\s*http:/?/?}{};
# TODO I think this is slightly duplicating the above data
# Note that it can default to empty though..
$self->{alias_url} = $conf->{roles}->{pastebot}->{alias_url};
$self->{session} = POE::Session->create(
object_states => [
$self => [ qw/_start display_page index display_paste receive_paste/ ]
]
);
eval {
$self->dbh->do(
"CREATE TABLE paste (
paste_id INTEGER PRIMARY KEY AUTOINCREMENT,
author VARCHAR(200),
summary VARCHAR(250),
paste LONGTEXT,
date_time INTEGER
)"
);
};
return $self;
}
# This method may be called as either a class method or an object method
# so we have to have some ugly branches to account for it.
sub dbh {
my( $self ) = @_;
if( ref $self and $self->{dbh} and $self->{dbh}->ping ) {
return $self->{dbh};
}
my $dbh = DBI->connect( "dbi:SQLite:dbname=var/pastes.db", "", "", {RaiseError => 1, PrintError => 0} )
or die "Failed to create DBI connection to var/pastes.db, this is a Big Problem! $!";
if( ref $self ) {
$self->{dbh} = $dbh;
}
return $dbh;
}
# This is a public method that can be called as a class method
# therefor $self may be a class or an object.
sub get_paste {
my( $self, $paste_id ) = @_;
my $paste = $self->dbh->selectrow_hashref(
"SELECT author,summary,paste,date_time FROM paste WHERE paste_id = ? LIMIT 1",
undef,
$paste_id
);
return $paste;
}
sub insert_paste {
my( $self, $nick, $summary, $paste )= @_;
my $dbh = $self->dbh;
$dbh->do(
"INSERT INTO paste
(author, summary, paste, date_time)
VALUES (?,?,?,?)
",
undef,
$nick,
$summary,
$paste,
time
);
my $id = $dbh->last_insert_id( undef, undef, undef, undef );
return $id;
}
sub _start {
my( $self, $kernel ) = @_[OBJECT,KERNEL];
my $conf = $self->{conf};
# Create it here so it acts as a child
$self->{server} = POE::Component::Server::SimpleHTTP->new(
PORT => $conf->{pastebot_plugin_port},
ADDRESS => $conf->{pastebot_plugin_addr} || undef,
ALIAS => 'pb_httpd_alias',
HANDLERS => [
{
DIR => '^/paste/\d+',
SESSION => "pastebot_role",
EVENT => "display_paste",
},
{
DIR => '^/paste_submit',
SESSION => "pastebot_role",
EVENT => "receive_paste",
},
{
DIR => '^/',
SESSION => "pastebot_role",
EVENT => "index",
},
]
);
$kernel->alias_set( "pastebot_role" );
$kernel->sig("DIE" => 'sig_DIE' );
}
sub display_page {
my( $self, $resp, $html ) = @_[OBJECT,ARG0,ARG1,ARG2];
warn "Display Page Activating: $resp\n";
$resp->code(RC_OK);
$resp->content_type("text/html");
$resp->content( $html );
$_[KERNEL]->post( pb_httpd_alias => 'DONE' => $resp );
}
sub index {
my( $self, $kernel, $req, $resp ) = @_[OBJECT,KERNEL,ARG0,ARG1];
my $template = Template->new;
my $channels = $kernel->call("Bot::BB3::Roles::IRC", "channel_list")
or warn "Failed to call: $!";
my $context = {
channels => $channels,
alias_url => $self->{alias_url},
};
my $output_html;
$template->process( \$INDEX_HTML, $context, \$output_html )
or warn "Failed to process: $Template::ERROR\n";
$_[KERNEL]->yield( display_page => $resp, $output_html );
}
sub display_paste {
my( $self, $req, $resp ) = @_[OBJECT,ARG0,ARG1];
my $output_html = "<body>Invalid Paste ID</body>";
$req->uri =~ m{paste/(\d+)}
or goto CLEANUP;
my $paste_id = $1;
my $paste = $self->get_paste( $paste_id );
if( not $paste or not keys %$paste ) { goto CLEANUP; }
my $template = Template->new;
my $context = {
author => $paste->{author},
summary => $paste->{summary},
paste => $paste->{paste},
date_time => $paste->{date_time},
};
undef $output_html;
$template->process( \$DISPLAY_PASTE_HTML, $context, \$output_html );
CLEANUP:
$_[KERNEL]->yield( display_page => $resp, $output_html );
}
sub receive_paste {
my( $self, $req, $resp ) = @_[OBJECT,ARG0,ARG1];
warn "Request: ", $req->content;
my $query = CGI->new( $req->content );
my $input = $query->param("body");
warn "Attempting to handle request: $req $resp $input\n";
my $id = $self->insert_paste(
$query->param("nick"),
$query->param("summary"),
$query->param("paste"),
time
);
my $template = Template->new;
my $context = {
map { $_ => $query->param( $_ ) } qw/nick summary channel paste/,
id => $id,
alias_url => $self->{alias_url},
};
my $output;
$template->process( \$RECEIVED_PASTE_HTML, $context, \$output )
or warn $Template::ERROR;
$_[KERNEL]->yield( display_page => $resp, $output );
my $alert_channel = $query->param("channel");
if( $alert_channel !~ /^\s*---/ ) { # Ignore things like "---irc.freenode, skip server names
my($server,$nick,$channel) = split /:/,$alert_channel,3;
my $external_url = $self->{alias_url} || $self->{hostname};
$_[KERNEL]->post( "Bot::BB3::Roles::IRC", external_message => $server, $nick, $channel,
( $context->{nick} || "Someone" )
. " pasted a new file at $external_url/paste/$id - $context->{summary}"
);
}
}
sub sig_DIE {
# Do nothing, we're ignoring fatal errors from our child, poco-server-simplehttp. I think we don't need to respawn them.
}
$INDEX_HTML = <<'END_HTML';
<html>
<head>
<style>
#summary {
width: 60em;
}
#paste {
width: 80em;
height: 25em;
}
</style>
</head>
<body>
<h2>Welcome to the BB3 Pastebot.</h2>
<ol>
<li>Select the channel for the URL announcment.</li>
<li>Supply a nick for the announcement.</li>
<li>Supply a summary of the paste for the announcement.</li>
<li>Paste!</li>
<li>Submit the form with the Paste it! button.</li>
</ol>
<form action="[% alias_url %]/paste_submit" method="post">
<ol>
<li style="float: left">Channel:
<select name="channel" id="channel">
[% FOREACH server IN channels.keys %]
<option name="channel">----[% server %] </option>
[% FOREACH nick IN channels.$server.keys %]
[% FOREACH channel IN channels.$server.$nick %]
<option value="[% server %]:[% nick %]:[% channel %]">[% channel %]</option>
[% END %]
[% END %]
[% END %]
</select>
</li>
<li style="float: left; clear: right; margin-left: 10em;">Nick:
<input type="text" name="nick" id="nick">
</li>
<li style="clear: both;">Summary:
<input type="text" name="summary" id="summary">
</li>
<li>Paste:
<textarea name="paste" id="paste"></textarea>
</li>
<li>
<input type="submit" name="paster" value="Paste It!">
<input type="reset" value="Clear Form">
</li>
</ol>
</form>
</body>
</html>
END_HTML
$RECEIVED_PASTE_HTML = <<'END_HTML';
<html>
<head>
<style>
</style>
</head>
<body>
Stored as: <a href="[% alias_url %]/paste/[% id %]">Paste [% id %]</a>
<br> [% summary %] by [% nick %] <br>
<pre>[% paste | html %]</pre>
</body>
</html>
END_HTML
$DISPLAY_PASTE_HTML = <<'END_HTML';
<html>
<head>
<style>
#paste {
width: 95%;
background-color: rgb(230,230,230);
}
</style>
</head>
<body>
<h2> BB3 PasteBot</h2>
<h3>From [% author | html %]</h3>
<h4>[% summary | html %]</h4>
<pre id="paste">[% paste | html %]</pre>
</body>
</html>
END_HTML
1;

View file

@ -1,86 +0,0 @@
package Bot::BB3::Roles::SocketMessageIRC;
use POE;
use POE::Wheel::SocketFactory;
use POE::Wheel::ReadWrite;
use Socket;
use strict;
sub new {
my( $class, $conf, $pm ) = @_;
my $self = bless { conf => $conf }, $class;
$self->{session} = POE::Session->create(
object_states => [
$self => [ qw/_start new_connection failed_connection read_line socket_error/ ]
]
);
return $self;
}
sub _start {
my( $self, $kernel ) = @_[OBJECT,KERNEL];
$kernel->alias_set( __PACKAGE__ );
$self->{socketfactory} = POE::Wheel::SocketFactory->new(
BindAddress => '127.0.0.1',
BindPort => ( $self->{conf}->{roles}->{socketmessageirc}->{port} || 10090 ),
SocketDomain => AF_INET(),
SocketType => SOCK_STREAM(),
SocketProtocol => 'tcp',
ListenQueue => 50,
Reuse => 'on',
SuccessEvent => 'new_connection',
FailureEvent => 'failed_connection',
);
}
sub new_connection {
my( $self, $socket ) = @_[OBJECT,ARG0];
my $wheel = POE::Wheel::ReadWrite->new(
Handle => $socket,
Driver => POE::Driver::SysRW->new,
Filter => POE::Filter::Line->new,
InputEvent => "read_line",
ErrorEvent => "socket_error",
);
$self->{rw_wheels}->{$wheel->ID} = $wheel; # save our reference
}
sub failed_connection {
}
sub read_line {
my( $self, $kernel, $line ) = @_[OBJECT,KERNEL,ARG0];
my( $server, $nick, $channel, $message ) = split/\s*:\s*/, $line, 4;
warn "Receiving irc message: $server,$nick,$channel,$message\n";
$kernel->post(
"Bot::BB3::Roles::IRC",
'external_message',
$server,
$nick,
$channel,
$message
);
}
sub socket_error {
my( $self, $wheel_id ) = @_[OBJECT,ARG3];
delete $self->{rw_wheels}->{$wheel_id};
}
1;

View file

@ -1,141 +0,0 @@
package Bot::BB3::Roles::Web;
use Bot::BB3::Logger;
use POE;
use POE::Component::Server::SimpleHTTP;
use HTTP::Status;
use CGI; #Heh.
use strict;
local $/;
my $HTML_TEMPLATE = <DATA>;
sub new {
my( $class, $conf, $plugin_manager ) = @_;
my $self = bless { conf => $conf, pm => $plugin_manager }, $class;
my $session = $self->{session} = POE::Session->create(
object_states => [
$self => [ qw/_start handle_request display_page plugin_output sig_DIE/ ]
]
);
return $self;
}
sub _start {
my( $self, $kernel ) = @_[OBJECT,KERNEL];
my $conf = $self->{conf};
warn '$conf{http}' . $conf->{http_plugin_port};
# Create it here so it acts as a child
$self->{server} = POE::Component::Server::SimpleHTTP->new(
PORT => $conf->{http_plugin_port},
ADDRESS => $conf->{http_plugin_addr} || undef,
ALIAS => 'web_httpd_alias',
HANDLERS => [
{
DIR => '^/request',
SESSION => "web_interface",
EVENT => "handle_request",
},
{
DIR => '^/',
SESSION => "web_interface",
EVENT => "display_page",
}
]
);
$kernel->alias_set( "web_interface" );
$kernel->sig("DIE" => 'sig_DIE' );
}
sub display_page {
my( $self, $req, $resp, $name, $output ) = @_[OBJECT,ARG0,ARG1,ARG2,ARG3];
my $html = $HTML_TEMPLATE;
warn "Display Page Activating: $req - $resp - $output\n";
if( $output ) {
$html =~ s/\%\%OUTPUT\%\%/$output/;
}
$resp->code(RC_OK);
$resp->content_type("text/html");
$resp->content( $html );
$_[KERNEL]->post( web_httpd_alias => 'DONE' => $resp );
}
my %RESP_MAP;
sub handle_request {
my( $self, $req, $resp, $name ) = @_[OBJECT,ARG0,ARG1,ARG2];
warn "Request: ", $req->content;
my $query = CGI->new( $req->content );
my $input = $query->param("body");
my @args = "2+2";
warn "Attempting to handle request: $req $resp $input\n";
# This is obviously silly but I'm unable to figure out
# the correct way to solve this =[
my $said = {
body => $input,
raw_body => $input,
my_name => 'WI',
addressed => 1,
recommended_args => \@args,
channel => '*web',
name => 'CC',
ircname => 'CC',
host => '*special', #TODO fix this to be an actual hostname!
# Make sure it isn't messed up by the alias feature..
server => '*special',
};
# Avoid passing around the full reference
$RESP_MAP{ "$resp" } = $resp;
$said->{pci_id} = "$resp";
$self->{pm}->yield( execute_said => $said );
}
sub plugin_output {
my( $self, $kernel, $said, $output ) = @_[OBJECT,KERNEL,ARG0,ARG1];
$output =~ s/^\s*CC://; # Clear the response name
my $resp = delete $RESP_MAP{ $said->{pci_id} };
$kernel->yield( display_page => undef, $resp, undef, $output );
}
sub sig_DIE {
# Do nothing, we're ignoring fatal errors from our child, poco-server-simplehttp. I think we don't need to respawn them.
}
1;
__DATA__
<html>
<head>
</head>
<body onload="document.getElementById('body_field').focus()">
Welcome to the BB3 web interface. You can interact with the bot by typing bot commands in to the text box below. It acts exactly as if you have typed the command in a private message to the bot. Try the command 'help' or 'plugins' (no quotes).
<form method="post" action="/request">
Input: <input type="text" name="body" id="body_field"> <input type="submit" value="go"> <br>
</form>
Output: %%OUTPUT%%
</body>
</html>

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,38 +0,0 @@
use Bot::BB3::MacroQuote ();
sub {
my( $said ) = @_;
my $flags = $said->{body};
my($quotemode, $wordnr, $auxfield) = ("z", 0, "macro_arg");
$flags =~ s/\&(\w+)// and
$auxfield = $1;
$flags =~ s/([a-zA-Z]+)// and
$quotemode = $1;
$flags =~ s/(-?[0-9]+)// and
$wordnr = $1;
my %auxfield_abbrev = (qw"
macro_arg macro_arg arg macro_arg a macro_arg
name name nick name n name
ircname ircname username ircname r ircname
host host h host
sender_raw sender_raw u sender_raw
channel channel c channel
by_chan_op by_chan_op o by_chan_op
server server s server network server
captured captured
");
my $f = $auxfield_abbrev{$auxfield};
my $str = $f && $said->{$f};
if (0 < $wordnr) {
$str = (split " ", $str)[$wordnr - 1];
} elsif ($wordnr < 0) {
$str = (split " ", $str, 1 - $wordnr)[-$wordnr];
}
print Bot::BB3::MacroQuote::quote($quotemode, $str);
}
__DATA__
Prints macro argument in a function macro factoid. Takes optional quoting mode letter or signed number for word splitting; or '&n' or '&c' etc to access extra info.

View file

@ -1,37 +0,0 @@
package Bot::BB3::Plugin::CacheCheck;
use strict;
sub new {
my( $class ) = @_;
my $self = bless {}, $class;
$self->{name} = "cache_check";
$self->{opts} = {
command => 1,
};
return $self;
}
sub initialize {
my( $self, $pm, $cache ) = @_;
die "Failed to receive a cache during initialization: $cache"
unless $cache;
$self->{cache} = $cache;
}
sub command {
my( $self, $said, $pm ) = @_;
my $cache = $self->{cache};
my $key = "cache_check_counter";
$cache->set( $key => ( $cache->get( $key ) + 1 ) );
return( 'handled', "Counter: " . $cache->get( $key ) );
}
"Bot::BB3::Plugin::CacheCheck";

View file

@ -1,117 +0,0 @@
package Bot::BB3::Plugin::Compose;
use strict;
sub new {
my( $class ) = @_;
my $self = bless {}, $class;
$self->{name} = 'compose';
$self->{opts} = {
command => 1,
};
return $self;
}
sub command {
my( $self, $said, $pm ) = @_;
my $results = compose( $said, $pm );
return('handled', $results);
}
# compose($body) does the main part of the composing,
# it should be in a module so both compose and factoid can call it.
# The calls should be wrapped around for security and stuff.
sub compose {
my($said, $pm) = @_;
my $str = $said->{body};
$str =~ /\A\s*((\S).*(\S))\s*\z/s or
return "Error: empty expression for compose";
my($expr, $openmark, $closemark) = ($1, $2, $3);
$openmark ne $closemark or
return "Error: identical open and close bracket marks for compose";
# we do things in two pass so we don't call any plugins if there are unbalanced parenthesis
my @toke;
my $depth = 0; my $finished = 0;
while ($expr =~ /\G(.*?)(?:(\Q$openmark\E)|\Q$closemark\E)/sg) {
my($part, $open) = ($1, defined($2));
$finished and
return "Error: unmatched closing parenthesis in compose";
push @toke, ["part", $part];
if ($open) {
push @toke, ["open"];
$depth++;
} else {
0 < --$depth or
$finished = 1;
0 <= $depth or
die "internal error: uncaught unmatched closing parenthesis in compose";
push @toke, ["close"];
}
}
0 == $depth or
return "Error: unmatched opening parenthesis in compose";
my @stack = ("");
for my $toke (@toke) {
my($op, $val) = @$toke;
if ("part" eq $op) {
$stack[-1] .= $val;
} elsif ("open" eq $op) {
push @stack, "";
} elsif ("close" eq $op) {
my $cmd = pop @stack;
#warn "cmd=|$cmd|";
my($success, $res) = runplugin($cmd, $said, $pm, 1 < @stack);
#warn "res=|$res|";
$success or
return $res;
$stack[-1] .= $res;
} else {
die "internal error: tokenizer found invalid token in compose";
}
}
1 == @stack or
die "internal error: execution stack unbalanced but the parenthesis were balanced in compose";
return $stack[0];
}
sub runplugin {
my( $cmd_string, $said, $pm, $captured ) = @_;
my( $cmd, $body ) = split " ", $cmd_string, 2;
defined($cmd) or
return( 0, "Error, cannot parse call to find command name, probably empty call in compose" );
defined($body) or $body = "";
my $plugin = $pm->get_plugin( $cmd )
or return( 0, "Compose failed to find a plugin named: $cmd" );
local $said->{body} = $body;
local $said->{recommended_args} = [ split /\s+/, $body ];
local $said->{command_match} = $cmd;
local $said->{nested} = 1; # everything called through compose is nested,
$captured and local $said->{captured} = 1;
# but things called on top-level of compose are captured only if the compose itself is captured
local $@;
my( $status, $results ) = eval { $plugin->command( $said, $pm ) };
if( $@ ) { return( 0, "Failed to execute plugin: $cmd because $@" ); }
else { return( 1, $results ) }
return( 0, "Error, should never reach here" );
}
"Bot::BB3::Plugin::Compose";
__DATA__
Supports composing multiple plugins together. That is, it allows you to feed the output of one plugin to another plugin. Syntax compose (eval (echo 2+2)). Note that it uses the first non whitespace character as the start-delimiter and the last non-whitespace as the end delimter.

View file

@ -1,51 +0,0 @@
use Data::Dumper;
sub {
my( $said, $pm ) = @_;
my $conf = $pm->get_main_conf;
my( $path, $value ) = split " ", $said->{body}, 2;
my $ref = $conf;
for( split /\./, $path ) {
if( ref $ref eq 'HASH' ) {
$ref = $ref->{$_};
}
elsif( ref $ref eq 'ARRAY' ) {
$ref = $ref->[$_];
}
else {
print "Errored out at $ref";
return;
}
}
if( not length $ref ) {
print "Failed to find element for $path; try conf_dump";
return;
}
if( not length $value ) {
if( ref $ref ) {
$Data::Dumper::Terse = 1;
print Dumper $ref;
}
else {
print $ref;
}
return;
}
print "Attempting to set [$path] to [$value] - $ref";
push @{$said->{special_commands}}, [ bb3_change_conf => $path, $value ];
}
__DATA__
conf <string> [new value]. Displays a portion of the conf structure corresponding to the dot seperated string passed to this plugin. For example, the string "bot.0" will display the complete structure of the first bot defined in the config file. Can also be used to set the value by passing a second argument after the location specifier. New values can be either a single string to set a scalar argumnet, or a comma seperated string surrounded by [], such as [foo,bar,baz]. White space around the commas are removed. This argument is turned in to an arrayref, in other words, a multivalued argument for the config option specified.

View file

@ -1,19 +0,0 @@
use Config::General;
sub {
my( $said, $pm ) = @_;
my $main_conf = $pm->get_main_conf;
my $o = Config::General->new(
-ConfigFile => $file,
-LowerCaseNames => 1,
-UseApacheInclude => 1,
-AutoTrue => 1
);
print $o->save_string( $main_conf );
}
__DATA__
Dump the current configuration file

View file

@ -1,25 +0,0 @@
use Module::CoreList;
sub {
my( $said, $pm ) = @_;
my $module = $said->{recommended_args}->[0];
my $rev = Module::CoreList->first_release( $module );
if( $rev ) { print "Added to perl core as of $rev" }
else {
my @modules = Module::CoreList->find_modules(qr/$module/);
if ( @modules ){
print 'Found', scalar @modules, ':', join ',' ,
map {$_.' in '. Module::CoreList->first_release( $_ ) } @modules;
}
else {
print "Module $module does not appear to be in core. Perhaps capitalization matters or try using the 'cpan' command to search for it." }
}
}
__DATA__
Tells you when the module you searched for was added to the Perl Core, if it was.

View file

@ -1,8 +0,0 @@
sub {
my( $said ) = @_;
print $said->{body};
}
__DATA__
Echo just prints its argument verbatim.

View file

@ -1,78 +0,0 @@
# eval plugin for buubot3
package Bot::BB3::Plugin::Eval;
package Bot::BB3::Plugin::Eval;
use POE::Filter::Reference;
use IO::Socket::INET;
use Data::Dumper;
use strict;
sub new {
my( $class ) = @_;
my $self = bless {}, $class;
$self->{name} = 'eval';
$self->{opts} = {
command => 1,
};
$self->{aliases} = [ qw/jseval jeval phpeval pleval perleval deparse k20eval rbeval pyeval luaeval/ ];
return $self;
}
sub command {
my( $self, $said, $pm ) = @_;
my $code = $said->{"body"};
my $type = $said->{command_match};
$type =~ s/^\s*(\w+?)eval/$1/;
warn "Initial type: $type\n";
my %translations = (
js => 'javascript',
perl => 'perl',
pl => 'perl',
php => 'php',
deparse => 'deparse',
'k20' => 'k20',
'k' => 'k20',
'rb' => 'ruby',
'ruby' => 'ruby',
'py' => 'python',
'python' => 'python',
'lua' => 'lua',
'j' => 'j',
);
$type = $translations{$type};
if( not $type ) { $type = 'perl'; }
warn "Found $type: $code";
my $filter = POE::Filter::Reference->new();
my $socket = IO::Socket::INET->new( PeerAddr => '127.0.0.1', PeerPort => '14400' )
or die "error: cannot connect to eval server";
my $refs = $filter->put( [ { code => "$type $code" } ] );
print $socket $refs->[0];
local $/;
my $output = <$socket>;
$socket->close;
my $result = $filter->get( [ $output ] );
my $resultstr = $result->[0]->[0];
if (!$said->{captured} && $resultstr !~ /\S/) {
$resultstr = "No output.";
}
$resultstr =~ s/\x0a?\x0d//g; # Prevent sending messages to the IRC server..
return( 'handled', $resultstr );
}
"Bot::BB3::Plugin::Eval";
__DATA__
The eval plugin. Evaluates various different languages. Syntax, eval: code; also pleval deparse rbeval jseval pyeval phpeval k20eval luaeval jeval.

View file

@ -1,501 +0,0 @@
package Bot::BB3::Plugin::Factoids;
use DBI;
use DBD::SQLite;
use POE::Component::IRC::Common qw/l_irc/;
use Text::Soundex qw/soundex/;
use strict;
use Data::Dumper;
my $COPULA = join '|', qw/is are was isn't were being am/, "to be", "will be", "has been", "have been", "shall be", "can has", "wus liek", "iz liek", "used to be";
my $COPULA_RE = qr/\b(?:$COPULA)\b/i;
sub new {
my( $class ) = @_;
my $self = bless {}, $class;
$self->{name} = 'factoids'; # Shouldn't matter since we aren't a command
$self->{opts} = {
command => 1,
#handler => 1,
};
$self->{aliases} = [ qw/fact call/ ];
return $self;
}
sub dbh {
my( $self ) = @_;
if( $self->{dbh} and $self->{dbh}->ping ) {
return $self->{dbh};
}
my $dbh = $self->{dbh} = DBI->connect(
"dbi:SQLite:dbname=var/factoids.db",
"",
"",
{ RaiseError => 1, PrintError => 0 }
);
return $dbh;
}
sub postload {
my( $self, $pm ) = @_;
my $sql = "CREATE TABLE factoid (
factoid_id INTEGER PRIMARY KEY AUTOINCREMENT,
original_subject VARCHAR(100),
subject VARCHAR(100),
copula VARCHAR(25),
predicate TEXT,
author VARCHAR(100),
modified_time INTEGER,
soundex VARCHAR(4),
compose_macro CHAR(1) DEFAULT '0',
protected BOOLEAN DEFAULT '0'
)"; # Stupid lack of timestamp fields
$pm->create_table( $self->dbh, "factoid", $sql );
delete $self->{dbh}; # UGLY HAX GO.
# Basically we delete the dbh we cached so we don't fork
# with one active
}
# This whole code is a mess.
# Essentially we need to check if the user's text either matches a
# 'store command' such as "subject is predicate" or we need to check
# if it's a retrieve command such as "foo" or if it's a retrieve sub-
# command such as "forget foo"
# Need to add "what is foo?" support...
sub command {
my( $self, $said, $pm ) = @_;
return unless $said->{body} =~ /\S/; #Try to prevent "false positives"
my $call_only = $said->{command_match} eq "call";
my $subject = $said->{body};
if( !$call_only and $subject =~ /\s+$COPULA_RE\s+/ ) {
my @ret = $self->store_factoid( $said );
return( 'handled', "Failed to store $said->{body}" )
unless @ret;
return ('handled', "@ret") if ($ret[0] =~ /^insuff/i);
return( 'handled', "Stored @ret" );
}
else {
my $commands_re = join '|', qw/search relearn learn forget revisions literal revert protect unprotect/;
$commands_re = qr/$commands_re/;
my $fact_string;
if( !$call_only && $subject =~ s/^\s*($commands_re)\s+// ) {
my( $cmd_name ) = "get_fact_$1";
$fact_string = $self->$cmd_name($subject, $said->{name}, $said);
}
else {
$fact_string = $self->get_fact( $pm, $said, $subject, $said->{name}, $call_only );
}
if( $fact_string ) {
return( 'handled', $fact_string );
}
else {
return;
}
}
}
sub _clean_subject {
my( $subject ) = @_;
$subject =~ s/^\s+//;
$subject =~ s/\s+$//;
$subject =~ s/\s+/ /g;
# $subject =~ s/[^\w\s]//g; #comment out to fix punct in factoids
$subject = lc $subject;
return $subject;
}
sub _clean_subject_func { # for parametrized macros
my($subject, $variant) = @_;
my( $key, $arg );
if ($variant) {
$subject =~ /\A\s*(\S+(?:\s+\S+)?)(?:\s+(.*))?\z/s or return;
( $key, $arg ) = ( $1, $2 );
} else {
$subject =~ /\A\s*(\S+)(?:\s+(.*))?\z/s or return;
( $key, $arg ) = ( $1, $2 );
}
$key =~ s/[^\w\s]//g;
return $key, $arg;
}
sub store_factoid {
my( $self, $said) =@_;
my ($author, $body ) = ($said->{name}, $said->{body});
return unless $body =~ /^(?:\S+[:,])?\s*(.+?)\s+($COPULA_RE)\s+(.+)$/s;
my( $subject, $copula, $predicate ) = ($1,$2,$3);
my $compose_macro = 0;
return "Insufficient permissions for changing protected factoid [$subject]" if (!$self->_db_check_perm($subject,$said));
if( $subject =~ s/^\s*\@?macro\b\s*// ) { $compose_macro = 1; }
elsif( $subject =~ s/^\s*\@?func\b\s*// ) { $compose_macro = 2; }
elsif( $predicate =~ s/^\s*also\s+// ) {
my $fact = $self->_db_get_fact( _clean_subject( $subject ), $author );
$predicate = $fact->{predicate} . " " . $predicate;
}
return unless
$self->_insert_factoid( $author, $subject, $copula, $predicate, $compose_macro, $self->_db_get_protect($subject) );
return( $subject, $copula, $predicate );
}
sub _insert_factoid {
my( $self, $author, $subject, $copula, $predicate, $compose_macro, $protected ) = @_;
my $dbh = $self->dbh;
warn "Attempting to insert factoid: type $compose_macro";
my $key;
if ( $compose_macro == 2 ) {
($key, my $arg) = _clean_subject_func($subject, 1);
warn "*********************** GENERATED [$key] FROM [$subject] and [$arg]\n";
$arg =~ /\S/
and return;
}
else {
$key = _clean_subject( $subject );
}
return unless $key =~ /\S/;
$dbh->do( "INSERT INTO factoid
(original_subject,subject,copula,predicate,author,modified_time,soundex,compose_macro,protected)
VALUES (?,?,?,?,?,?,?,?,?)",
undef,
$key,
$subject,
$copula,
$predicate,
l_irc($author),
time,
soundex($key),
$compose_macro || 0,
$protected || 0,
);
return 1;
}
sub get_fact_protect {
my( $self, $subject, $name, $said ) = @_;
warn "===TRYING TO PROTECT [$subject] [$name]\n";
#XXX check permissions here
return "Insufficient permissions for protecting factoid [$subject]" if (!$self->_db_check_perm($subject,$said));
my $fact = $self->_db_get_fact( _clean_subject( $subject ), $name );
if (defined($fact->{predicate}))
{
$self->_insert_factoid( $name, $subject, $fact->{copula}, $fact->{predicate}, $fact->{compose_macro}, 1 );
return "Protected [$subject]";
}
else
{
return "Unable to protect nonexisting factoid [$subject]";
}
}
sub get_fact_unprotect {
my( $self, $subject, $name, $said ) = @_;
warn "===TRYING TO PROTECT [$subject] [$name]\n";
#XXX check permissions here
return "Insufficient permissions for unprotecting factoid [$subject]" if (!$self->_db_check_perm($subject,$said));
my $fact = $self->_db_get_fact( _clean_subject( $subject ), $name );
if (defined($fact->{predicate}))
{
$self->_insert_factoid( $name, $subject, $fact->{copula}, $fact->{predicate}, $fact->{compose_macro}, 0 );
return "Unprotected [$subject]";
}
else
{
return "Unable to unprotect nonexisting factoid [$subject]";
}
}
sub get_fact_forget {
my( $self, $subject, $name, $said ) = @_;
warn "===TRYING TO FORGET [$subject] [$name]\n";
#XXX check permissions here
return "Insufficient permissions for forgetting protected factoid [$subject]" if (!$self->_db_check_perm($subject,$said));
$self->_insert_factoid( $name, $subject, "is", " ", 0, $self->_db_get_protect($subject) );
return "Forgot $subject";
}
sub _fact_literal_format {
my($r) = @_;
($r->{protected}?"P:" : "" ).
("","macro ","func ")[$r->{compose_macro}] .
"$r->{subject} $r->{copula} $r->{predicate}";
}
sub get_fact_revisions {
my( $self, $subject, $name ) = @_;
my $dbh = $self->dbh;
my $revisions = $dbh->selectall_arrayref(
"SELECT factoid_id, subject, copula, predicate, author, compose_macro, protected
FROM factoid
WHERE original_subject = ?
ORDER BY modified_time DESC
", # newest revision first
{Slice=>{}},
_clean_subject( $subject ),
);
my $ret_string = join " ", map {
"[$_->{factoid_id} by $_->{author}: " . _fact_literal_format($_) . "]";
} @$revisions;
return $ret_string;
}
sub get_fact_literal {
my( $self, $subject, $name ) = @_;
my $fact = $self->_db_get_fact( _clean_subject( $subject ), $name );
return _fact_literal_format($fact);
}
sub get_fact_revert {
my( $self, $subject, $name, $said ) = @_;
my $dbh = $self->dbh;
#XXX check permissions here
return "Insufficient permissions for reverting protected factoid [$subject]" if (!$self->_db_check_perm($subject,$said));
$subject =~ s/^\s*(\d+)\s*$//
or return "Failed to match revision format";
my $rev_id = $1;
my $fact_rev = $dbh->selectrow_hashref(
"SELECT subject, copula, predicate, compose_macro
FROM factoid
WHERE factoid_id = ?",
undef,
$rev_id
);
my $protect = $self->_db_get_protect($fact_rev->{subject});
return "Bad revision id" unless $fact_rev and $fact_rev->{subject}; # Make sure it's valid..
# subject, copula, predicate
$self->_insert_factoid( $name, @$fact_rev{qw"subject copula predicate compose_macro"}, $protect);
return "Reverted $fact_rev->{subject} to revision $rev_id";
}
sub get_fact_learn {
my( $self, $body, $name, $said ) = @_;
$body =~ s/^\s*learn\s+//;
my( $subject, $predicate ) = split /\s+as\s+/, $body, 2;
#XXX check permissions here
return "Insufficient permissions for changing protected factoid [$subject]" if (!$self->_db_check_perm($subject,$said));
#my @ret = $self->store_factoid( $name, $said->{body} );
$self->_insert_factoid( $name, $subject, 'is', $predicate, 0 , $self->_db_get_protect($subject));
return "Stored $subject as $predicate";
}
*get_fact_relearn = \&get_fact_learn; #Alias..
sub get_fact_search {
my( $self, $body, $name ) = @_;
my $results = $self->dbh->selectall_arrayref(
"SELECT subject,copula,predicate
FROM factoid
WHERE subject like ?
GROUP BY subject", # Group by magically returns the right row first. I dunno.
{Slice => {}},
"%$body%",
);
if( $results and @$results ) {
my $ret_string;
for( @$results ) {
$ret_string .= "[" . _fact_literal_format($_) . "] ";
}
return $ret_string;
}
else {
return "No matches."
}
}
sub get_fact {
my( $self, $pm, $said, $subject, $name, $call_only ) = @_;
return $self->basic_get_fact( $pm, $said, $subject, $name, $call_only );
}
sub _db_check_perm {
my ($self, $subj, $said) = @_;
my $isprot = $self->_db_get_protect($subj);
warn "Checking permissions of [$subj] for [$said->{name}]";
warn Dumper($said);
#always refuse to change factoids if not in one of my channels
return 0 if (!$said->{in_my_chan});
#if its not protected no need to check if they are op or root;
return 1 if (!$isprot);
if ($isprot && ($said->{by_root} || $said->{by_chan_op}))
{
return 1;
}
#default case, $isprotect true; op or root isn't
return 0;
}
#get the status of the protection bit
sub _db_get_protect {
my( $self, $subj ) = @_;
$subj = _clean_subject($subj,1);
my $dbh = $self->dbh;
my $prot = ($dbh->selectrow_array( "
SELECT protected
FROM factoid
WHERE original_subject = ?
ORDER BY factoid_id DESC
",
undef,
$subj,
))[0];
return $prot;
}
sub _db_get_fact {
my( $self, $subj, $name ) = @_;
my $dbh = $self->dbh;
my $fact = $dbh->selectrow_hashref( "
SELECT factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected
FROM factoid
WHERE original_subject = ?
ORDER BY factoid_id DESC
",
undef,
$subj,
);
return $fact;
}
sub basic_get_fact {
my( $self, $pm, $said, $subject, $name, $call_only ) = @_;
my ($fact, $key, $arg);
my $key = _clean_subject($subject);
my $fact;
if( !$call_only ) {
$fact = $self->_db_get_fact($key, $name);
}
# Attempt to determine if our subject matches a previously defined
# 'macro' or 'func' type factoid.
# I suspect it won't match two word function names now.
for my $variant (0, 1) {
if (!$fact) {
($key, $arg) = _clean_subject_func($subject, $variant);
$fact = $self->_db_get_fact($key, $name, 1);
}
}
if( $fact->{predicate} =~ /\S/ ) {
if( $fact->{compose_macro} ) {
my $plugin = $pm->get_plugin("compose");
local $said->{macro_arg} = $arg;
local $said->{body} = $fact->{predicate};
local $said->{addressed} = 1; # Force addressed to circumvent restrictions? May not be needed!
return $plugin->command($said,$pm);
}
else {
return "$fact->{predicate}";
}
}
else {
my $soundex = soundex( _clean_subject($subject, 1) );
my $matches = $self->_soundex_matches( $soundex );
if( $matches and @$matches ) {
return "No factoid found. Did you mean one of these: " . join " ", map "[$_]", @$matches;
}
else {
return;
}
}
}
sub _soundex_matches {
my( $self, $soundex ) = @_;
my $dbh = $self->dbh;
my $rows = $dbh->selectall_arrayref(
"SELECT factoid_id,subject,predicate FROM factoid WHERE soundex = ? GROUP BY subject LIMIT 10",
undef,
$soundex
);
return [ map $_->[1], grep $_->[2] =~ /\S/, @$rows ];
}
"Bot::BB3::Plugin::Factoids";
__DATA__
Learn or retrieve persistent factoids. "foo is bar" to store. "foo" to retrieve. try "forget foo" or "revisions foo" or "literal foo" or "revert $REV_ID" too. "macro foo is [echo bar]" or "func foo is [echo bar [arg]]" for compose macro factoids. The factoids/fact/call keyword is optional except in compose. Search <subject> to search for factoids that match.

View file

@ -1,18 +0,0 @@
use Geo::IP;
sub {
my( $said, $pm ) = @_;
my $gi = Geo::IP->new(GEOIP_STANDARD);
print "Record for $said->{body}: ";
if( $said->{body} =~ /[a-zA-Z]/ ) {
print $gi->country_code_by_name( $said->{body} );
}
else {
print $gi->country_code_by_addr( $said->{body} );
}
}
__DATA__
geoip 192.168.32.45 or geoip example.com; returns the country associated with the resolved IP address.

View file

@ -1,31 +0,0 @@
use LWP::UserAgent;
sub {
my( $said ) = @_;
my $ua = LWP::UserAgent->new( agent => "BB3WebAgent! (mozilla)" );
my $url;
if( $said->{body} =~ m{(http://\S+)} ) {
$url = $1;
}
elsif( $said->{body} =~ /(\S+)/ ) {
$url = "http://$1";
}
else {
print "That doesn't look like a url..";
return;
}
my $resp = $ua->head( $url );
if( not $resp ) {
print "Couldn't fetch [$url] you failure";
return;
}
print "$url: " . $resp->code . ": " . $resp->message . ". " . $resp->header("server");
}
__DATA__
head http://url/; returns the response code and server type from a HEAD request for a particular url.

View file

@ -1,24 +0,0 @@
use strict;
sub {
my( $said, $pm ) = @_;
my $plugin_name = $said->{recommended_args}->[0];
if( length $plugin_name ) {
my $plugin = $pm->get_plugin( $plugin_name );
if( $plugin ) {
print $plugin->{help_text};
}
else {
print "Sorry, no plugin named $plugin_name found.";
}
}
else {
print "Provides help text for a specific command. Try 'help echo'. See also the command 'plugins' to list all of the currently loaded plugins.";
}
}
__DATA__
Attempts to find the help for a plugin. Syntax help PLUGIN.

View file

@ -1,47 +0,0 @@
#!/usr/bin/perl
use Net::DNS;
use strict;
use Data::Dumper;
my $foo=sub {
my( $said, $pm ) = @_;
my $host = $said->{recommended_args}->[0];
my $recordtype = $said->{recommended_args}->[1];
print "Couldn't find a host to check!" and return
unless $host;
$recordtype ||= "A";
my $res = Net::DNS::Resolver->new;
my $query = $res->query($host, $recordtype);
if ($query)
{
my @resu;
foreach my $rr ($query->answer)
{
next unless $rr->type eq $recordtype;
push @resu, $rr->string;
}
print "No $recordtype record found for $host" and return if (!@resu);
s/\s+/ /g for @resu;
print join(" :: ", @resu) and return;
}
else
{
print "query failed: ", $res->errorstring;
}
};
if ($0 =~ /host.pm$/)
{
$foo->({recommended_args=>['google.com','A']});
}
else
{
$foo;
}
__DATA__
Returns information about a host's DNS records

View file

@ -1,14 +0,0 @@
use DBI;
sub {
my( $said, $pm ) = @_;
my $dbh = DBI->connect("dbi:SQLite:dbname=var/hosts.db");
my $recs = $dbh->selectall_arrayref( "SELECT * FROM hosts where host = ?", {Slice=>{}}, $said->{body} );
print "$said->{body}: ", join ", ", map $_->{nick}, @$recs;
}
__DATA__
host_lookup <hostname>. Returns all of the nicks this bot has seen using the host name you specify.

View file

@ -1,13 +0,0 @@
sub {
my( $said, $pm ) = @_;
my @channels = grep /^#/, @{ $said->{recommended_args} };
push @{ $said->{special_commands} },
[ 'pci_join', @channels ];
print "Joining @{ $said->{recommended_args} }";
}
__DATA__
Attempts to join a list of channels. Syntax join #foo #bar #baz. Typically requires op or superuser.

View file

@ -1,42 +0,0 @@
use POE::Component::IRC::Common qw/l_irc/;
use DBI;
use DBD::SQLite;
sub {
my( $said, $pm ) = @_;
my $body = $said->{body};
s/^\s+//, s/\s+$// for $body;
warn "KARMAPLUGIN";
use Data::Dumper;
warn Dumper $said;
my $dbh = DBI->connect(
"dbi:SQLite:dbname=var/karma.db",
"",
"",
{ RaiseError => 1, PrintError => 0 }
);
my $lirc = l_irc($said->{body}) || lc $said->{body};
warn "SUBJECT: $lirc";
my $karma = $dbh->selectrow_arrayref(
"SELECT sum(operation) FROM karma WHERE subject = ?",
undef,
$lirc,
);
if( $karma and @$karma) {
if ($karma->[0])
{
print "$body has karma of $karma->[0]";
}
else
{
print "$body has no karma";
}
}
}
__DATA__
karma <nickname>; returns the "karma" value for a user or arbitrary subject. Karma works by appending either ++ or -- to a word to modify its karma.

View file

@ -1,74 +0,0 @@
package Bot::BB3::Plugin::Karma_Modify;
use POE::Component::IRC::Common qw/l_irc/;
use DBI;
use DBD::SQLite;
sub new {
my( $class ) = @_;
my $self = bless {}, $class;
$self->{name} = "karma_modify"; # This shouldn't be necessary
$self->{opts}->{handler} = 1;
return $self;
}
sub dbh {
my( $self ) = @_;
if( $self->{dbh} and $self->{dbh}->ping ) {
return $self->{dbh};
}
my $dbh = $self->{dbh} = DBI->connect(
"dbi:SQLite:dbname=var/karma.db",
"",
"",
{ RaiseError => 1, PrintError => 0 }
);
return $dbh;
}
sub postload {
my( $self, $pm ) = @_;
my $sql = "CREATE TABLE karma (
karma_id INTEGER PRIMARY KEY AUTOINCREMENT,
subject VARCHAR(250),
operation TINYINT,
author VARCHAR(32),
modified_time INTEGER
)"; # Stupid lack of timestamp fields
$pm->create_table( $self->dbh, "karma", $sql );
delete $self->{dbh}; # UGLY HAX GO.
# Basically we delete the dbh we cached so we don't fork
# with one active
}
sub handle {
my( $self, $said, $pm ) = @_;
my $body = $said->{body};
if( $body =~ /\(([^\)]+)\)(\+\+|--)/ or $body =~ /(\w+)(\+\+|--)/ ) {
my( $subject, $op ) = ($1,$2);
if( $op eq '--' ) { $op = -1 } elsif( $op eq '++' ) { $op = 1 }
my $lirc = l_irc($subject) || lc $subject;
$self->dbh->do( "INSERT INTO karma (subject,operation,author,modified_time) VALUES (?,?,?,?)",
undef,
$lirc,
$op,
$said->{name},
scalar time,
);
}
return;
}
"Bot::BB3::Plugin::Karma_Modify";

View file

@ -1,63 +0,0 @@
package Bot::BB3::Plugin::More;
use strict;
sub new {
my( $class ) = @_;
my $self = bless {}, $class;
$self->{name} = 'more';
$self->{opts} = {
command => 1,
post_process => 1,
};
return $self;
}
sub initialize {
my( $self, $pm, $cache ) = @_;
$self->{cache} = $cache;
}
sub command {
my( $self, $said, $pm ) = @_;
my $text = $self->{cache}->get( "pager_$said->{name}" );
$self->{cache}->remove( "pager_$said->{name}" );
if( $text ) { return( 'handled', "...$text" ); }
else { return( 'handled', "Sorry, no more output" ); }
}
sub post_process {
my( $self, $said, $pm, $output_ref ) = @_;
return if $said->{channel} =~ /^\*/;
# Magic numbers are awesome.
# the usual max length for an irc message is around 425?
# Something like that.
# The actual max is usually 512 but you need room for nicks and command types.
if( length $$output_ref > 400 ) {
# Sanity checking, let's not store novels.
if( length $$output_ref > 1_000 ) {
my $new_out = $$output_ref = substr( $$output_ref, 0, 1_000 );
$$output_ref = $new_out;
warn "Sanity checking, new length: ", length $$output_ref;
}
my $new_text = substr( $$output_ref, 0, 350, '' );
$self->{cache}->set( "pager_$said->{name}", $$output_ref, "10 minutes" ); #Remainder
$$output_ref = $new_text;
$$output_ref .= "... [Output truncated. Use `more` to read more]";
}
}
"Bot::BB3::Plugin::More";
__DATA__
More acts as a pager. It automatically truncates output that is too long and saves it in a buffer based on your name. Use the command `more` to access the remainder of the text.

View file

@ -1,14 +0,0 @@
use DBI;
sub {
my( $said, $pm ) = @_;
my $dbh = DBI->connect("dbi:SQLite:dbname=var/hosts.db");
my $recs = $dbh->selectall_arrayref( "SELECT * FROM hosts where nick = ?", {Slice=>{}}, $said->{body} );
print "$said->{body}: ", join ", ", map $_->{host}, @$recs;
}
__DATA__
nick_lookup <nickname>; returns all of the hostnames this bot has seen a particular nick name use.

View file

@ -1,12 +0,0 @@
sub {
my( $said, $pm ) = @_;
push @{$said->{special_commands}},
map { [ pci_part => $_ ] } @{$said->{recommended_args}}
;
print "Attempting to leave: @{$said->{recommended_args}} ";
}
__DATA__
Attempts to leave a list of channels. Syntax, part #foo #bar #baz. Note, does no sanity checking. Typically requires op or superuser.

View file

@ -1,23 +0,0 @@
package Bot::BB3::Plugin::Plugins;
use strict;
sub new {
my($class) = @_;
my $self = bless {}, $class;
$self->{"name"} = "plugins";
$self->{"opts"}->{"command"} = 1;
return $self;
}
sub command {
my($self, $said, $manager) = @_;
my $output = join(" ", sort map { $_->{name} } @{$manager->get_plugins});
#return( "handled", $output );
return( "handled", $output );
}
"Bot::BB3::Plugin::Plugins";
__DATA__
Returns a list of all of the loaded plugins for this bot. Syntax, plugins

View file

@ -1,14 +0,0 @@
use Bot::BB3::MacroQuote ();
sub {
my( $said ) = @_;
$said->{body} =~ /\A\s*(\w+)\s?(.*)\z/s # note: only one space after the quoting mode so we can quote strings starting with space
or return;
my($mode, $str) = ($1, $2);
print Bot::BB3::MacroQuote::quote($mode, $str);
}
__DATA__
Escape a string to prepare for interpolation in an eval program code. Syntax is quote m string, with one space after quoting mode m which can be: z (no-op), c (c-like hex escapes), d (with delimiters), e, f, h.

View file

@ -1,14 +0,0 @@
sub {
my( $said, $pm ) = @_;
push @{ $said->{special_commands} },
[ pm_reload_plugins => 1 ]
;
print "Attempting to reload plugins...";
}
__DATA__
Attempts to reload all of the plugins in the plugin directory. Has the effect of reloading any changed plugins or adding any new ones that have been added. Typically root only.

View file

@ -1,10 +0,0 @@
sub {
my( $said, $pm ) = @_;
push @{$said->{special_commands}}, [ bb3_restart=> 1 ];
print "Attempting to restart..";
}
__DATA__
restart. Attempts to rexecute the bot in the exact manner it was first execute. This has the effect of reloading all config files and associated plugins. Typically root only.

View file

@ -1,22 +0,0 @@
use XML::RSS::Parser;
use strict;
sub {
my( $said, $pm ) = @_;
my $feed_uri = $said->{recommended_args}->[0];
print "Couldn't find a url to fetch!" and return
unless $feed_uri;
my $parser = XML::RSS::Parser->new;
my $feed = $parser->parse_uri( $feed_uri ) #TODO check for http:// schema
or ( print "Couldn't parse $feed_uri because", $parser->errstr and return );
for( $feed->query("//item/title") ) {
print $_->text_content;
}
}
__DATA__
Returns small list of headlines from an RSS feed. Syntax, fetch_rss http://example/rss

View file

@ -1,20 +0,0 @@
use XML::RSS::Parser;
use strict;
sub {
my( $said, $pm ) = @_;
my $feed_uri = $said->{recommended_args}->[0];
print "Couldn't find a url to fetch!" and return
unless $feed_uri;
my $parser = XML::RSS::Parser->new;
my $feed = $parser->parse_uri( $feed_uri ) #TODO check for http:// schema
or ( print "Couldn't parse $feed_uri because", $parser->errstr and return );
print +($feed->query("//item/title"))[0]->text_content;
}
__DATA__
Returns the first headline from a specified RSS feed.

View file

@ -1,27 +0,0 @@
use WWW::RottenTomatoes;
# Construct it outside the sub so it can at least pretend to do some caching.
my $rt = WWW::RottenTomatoes->new;
sub {
my( $said, $pm ) = @_;
local $@;
my $movie_info = eval { $rt->movie_info( $said->{body} ) };
if( $@ ) {
print "Error fetching movie info: $@";
return;
}
if( $movie_info ) {
print "$movie_info->{title}: $movie_info->{rating} - ", @{ $movie_info->{bubbles} }[ rand @{ $movie_info->{bubbles} } ];
}
else {
print "Sorry failed to find a movie titled [$said->{body}]";
}
}
__DATA__
RottenTomatoes plugin. Syntax, rt Movie Title.

View file

@ -1,10 +0,0 @@
sub {
my( $said, $pm ) = @_;
push @{$said->{special_commands}}, [ bb3_save_main_conf => 1 ];
print "Attempting to save my config, note that it won't change anything until I restart.";
}
__DATA__
save_config. Attempts to write the current config structure back to the file it was originally read from. Hopefully you've modified it first. Note, may lose comments. Root only.

View file

@ -1,94 +0,0 @@
package Bot::BB3::Plugin::Seen;
use POE::Component::IRC::Common qw/l_irc/;
use DBD::SQLite;
use strict;
sub new {
my( $class ) = @_;
my $self = bless {}, $class;
$self->{name} = "seen";
$self->{opts} = {
command => 1,
handler => 1,
};
return $self;
}
sub dbh {
my( $self ) = @_;
if( $self->{dbh} and $self->{dbh}->ping ) {
return $self->{dbh};
}
my $dbh = $self->{dbh} = DBI->connect( "dbi:SQLite:dbname=var/seen.db", "", "", { PrintError => 0, RaiseError => 1 } );
return $dbh;
}
sub postload {
my( $self, $pm ) = @_;
my $sql = "CREATE TABLE seen (
seen_id INTEGER PRIMARY KEY AUTOINCREMENT,
user VARCHAR(25),
lc_user VARCHAR(25),
message VARCHAR(250),
seen_date INTEGER
);";
$pm->create_table( $self->dbh, "seen", $sql );
delete $self->{dbh}; # UGLY HAX GO.
# Basically we delete the dbh we cached so we don't fork
# with one active
}
sub command {
my( $self, $said, $pm ) = @_;
my( $target ) = @{ $said->{recommended_args} };
my $seen = $self->dbh->selectrow_arrayref( "SELECT user,message,seen_date FROM seen WHERE lc_user = ?",
undef,
l_irc( $target )
);
if( $seen and @$seen and $seen->[0] ) {
return( 'handled', "I last saw $seen->[0] saying \"$seen->[1]\" at " . gmtime($seen->[2]) . " Z." );
}
else {
return( 'handled', "I don't think I've seen $target." );
}
}
sub handle {
my ( $self, $said, $pm ) = @_;
my $count = $self->dbh->do( "UPDATE seen SET user = ?, message = ?, seen_date = ? WHERE lc_user = ?",
undef,
$said->{name},
$said->{body},
time(),
l_irc( $said->{name} ),
);
if( $count == 0 ) {
$self->dbh->do( "INSERT INTO seen (user,lc_user,message,seen_date) VALUES ( ?,?,?,? )",
undef,
$said->{name},
l_irc($said->{name}),
$said->{body},
time(),
);
}
return;
}
"Bot::BB3::Plugin::Seen";
__DATA__
The seen plugin. Attempts to keep track of every user the bot has 'seen'. Use the syntax, seen user; to ask the bot when it last saw the user named 'user'.

View file

@ -1,11 +0,0 @@
use WWW::Shorten 'Metamark';
sub {
my( $said, $pm ) = @_;
print "New link: ", makeashorterlink($said->{body});
}
__DATA__
shorten <url> returns the "short form" of a url. Defaults to using xrl.us.

View file

@ -1,67 +0,0 @@
package Bot::BB3::Plugin::Spell;
use Text::Aspell;
use strict;
sub new {
my( $class ) = @_;
my $self = bless {}, $class;
$self->{name} = 'spell';
$self->{opts} = {
command => 1,
handler => 1,
};
return $self;
}
sub handle {
my( $self, $said, $pm ) = @_;
my( undef, $ret ) = $self->_speller( $said, $pm, 'handle' );
# TODO fix this, we just get rid of 'handled'.
# Need to clean up this code to remove this.
return $ret;
}
sub command {
my( $self, $said, $pm ) = @_;
return $self->_speller( $said, $pm, 'command' );
}
sub _speller {
my( $self, $said, $pm, $type ) = @_;
my $speller = Text::Aspell->new
or die "Couldn't create a speller!";
$speller->set_option('lang','en_GB');
my $word;
if( $type eq 'command' ) { #Command Mode
$word = $said->{recommended_args}->[0];
}
else { #Text Search Mode
if( $said->{body} =~ /(\w+)\s*\(sp\??\)/ ) {
$word = $1;
}
}
if( $word ) {
if( $speller->check($word) ) {
return( 'handled', "$word seems to be correct!" );
}
else {
return( 'handled', "$word seems to be misspelt, perhaps you meant: " . join " ", $speller->suggest( $word ) );
}
}
return;
}
"Bot::BB3::Plugin::Spell";
__DATA__
Attempt to determine the correct spelling of a word. Operates in two modes, addressed, via the syntax spell word; or in passing when you use the string '(sp?)', without the quotes, after any word in any sentence.

View file

@ -1,57 +0,0 @@
# ------------------------
# Originally by Mauke at: http://mauke.ath.cx/stuff/perl/unip
# ------------------------
use Unicode::UCD 'charinfo';
use Encode qw/decode encode_utf8/;
use utf8;
use strict;
sub speng {
my $x = shift;
$x =~ /^0[0-7]+\z/ and return oct $x;
$x =~ /^(?:[Uu]\+|0[Xx])([[:xdigit:]]+)\z/ || (
length($x) > 1 && $x =~ /^([[:xdigit:]]*[A-Fa-f][[:xdigit:]]*)\z/
) and return hex $1;
$x =~ /^[0-9]+\z/ and return $x;
return map ord, split //, $x
}
sub unip {
my @pieces = @_;
my (@out, @err);
for (@pieces) {
my $chr = chr;
my $utf8 = join ' ', unpack '(H2)*', encode_utf8 $chr;
my $x;
unless ($x = charinfo $_) {
push @err, sprintf "U+%X (%s): no match found", $_, $utf8;
next;
}
push @out, "U+$x->{code} ($utf8): $x->{name} [$chr]";
}
\@err, \@out
}
# ------------------------
sub {
my( $said, $pm ) = @_;
utf8::decode( $said->{body} );
my ($err, $out) = unip map speng($_), split " ", $said->{body};
utf8::upgrade( $_ ) for @$err, @$out;
print "Error: @$err\n" if @$err;
print "@$out\n";
}
__DATA__
unicode U+2301; returns the unicode character and associated information given either a unicode character or one of the various ways you can specify a code point.

View file

@ -1,62 +0,0 @@
use Geo::WeatherNWS;
use Weather::Underground;
use Data::Dumper;
use Geo::IATA;
use List::AllUtils qw/first/;
# Stefan Petrea
# stefan.petrea at gmail.com
# perlhobby.googlecode.com
my $solve_weather = sub {
my $arg = shift;
my $g = Geo::IATA->new;
my $location = first { defined $_->{icao} } @{$g->location($arg)};
my $weather = Weather::Underground->new( place => $location->{icao}, debug => 0 );
my $data = $weather->get_weather;
return ($weather,$data,$location->{location});
};
sub {
my( $said, $pm ) = @_;
my $arg = $said->{body};
s/^\s+//,s/\s+$// for $arg;
if( $arg =~ /^[kK]/ or $arg =~ /^\w{3}$/ ) {
my $w= Geo::WeatherNWS->new;
$w->getreporthttp( $arg );
print "$w->{code}: $w->{temperature_f} degrees, $w->{conditionstext} with a windchill of $w->{windchill_f}f and winds up to $w->{windspeedmph}mph";
}
else {
my $weather = Weather::Underground->new( place => $arg, debug => 0 );
my $data = $weather->get_weather;
my $resolved_location = "";
($weather,$data,$resolved_location) = $solve_weather->($arg) unless $data; # fix it if we have a problem
if( not $data or not @$data ) {
print "Failed to find weather for $arg";
return;
};
$data = $data->[0]; # We want the first one..
my $where =
$resolved_location
? "Resolved location->{$resolved_location}: "
: "$arg:";
print "$where $data->{temperature_fahrenheit} degrees, $data->{conditions} and winds up to $data->{wind_milesperhour}";
}
}
__DATA__
weather <zipcode> or weather <airport code>; attempts to retrieve the weather from a station associated with one of the names you pass it.

View file

@ -1,5 +0,0 @@
K 13
svn:mergeinfo
V 0
END

View file

@ -1,266 +0,0 @@
10
dir
475
svn://erxz.com/bb3/branches/perlbuut/var
svn://erxz.com/bb3
2009-10-03T22:53:42.528878Z
475
simcop
has-props
dcb1cea6-7f7e-4c78-8a22-148ace8ce36e
karma.db
file
489
2009-11-24T20:34:38.000000Z
70ac6e5d8ac91f6750881c984b73713a
2009-11-24T21:26:47.850539Z
489
simcop
has-props
724992
factoids.db
file
488
2009-11-24T00:55:30.000000Z
62e89e9dfeff0a4424a163d1d03b2660
2009-11-24T11:22:27.703097Z
488
simcop
has-props
637952
pastes.db
file
488
2009-10-03T23:06:00.000000Z
7bba0ac438f61ea44c12a29617f77cc5
2009-11-24T11:22:27.703097Z
488
simcop
has-props
3072
hosts.db
file
488
2009-10-03T23:06:00.000000Z
d41d8cd98f00b204e9800998ecf8427e
2009-11-24T11:22:27.703097Z
488
simcop
0
tell.db
file
488
2009-10-03T23:06:00.000000Z
d869be1017f18a7bc7262d352ee8518a
2009-11-24T11:22:27.703097Z
488
simcop
has-props
3072
full_units.storable
file
2009-10-03T22:54:20.000000Z
934aecb527f0d24ad0a2e84b2d4564c4
2009-10-03T22:53:42.528878Z
475
simcop
has-props
870351
seen.db
file
489
2009-11-24T21:30:07.000000Z
dc84fe3544deb9ec7a067de035060613
2009-11-24T21:26:47.850539Z
489
simcop
has-props
6144

View file

@ -1,5 +0,0 @@
K 13
svn:mime-type
V 24
application/octet-stream
END

View file

@ -1,5 +0,0 @@
K 13
svn:mime-type
V 24
application/octet-stream
END

View file

@ -1,5 +0,0 @@
K 13
svn:mime-type
V 24
application/octet-stream
END

View file

@ -1,5 +0,0 @@
K 13
svn:mime-type
V 24
application/octet-stream
END

View file

@ -1,5 +0,0 @@
K 13
svn:mime-type
V 24
application/octet-stream
END

View file

@ -1,5 +0,0 @@
K 13
svn:mime-type
V 24
application/octet-stream
END

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more