#! /usr/bin/perl use strict; use warnings; my $name = 'cbstream'; my $version = 0.08; # what I need... use YAML::Syck; use Data::Diver qw(DiveVal Dive DiveRef); use LWP::Simple qw(); use XML::Twig; # debugging tools... use IO::Handle; open my $log, '>', File::Spec->catfile( Xchat::get_info('xchatdir'), "$name.log" ) or die "Can't open $name.log: $!"; $log->autoflush(); sub LOG { print $log scalar localtime, ": ", @_, "\n" } LOG 'starting up...'; use Data::Dumper; =head1 NAME CBStream Xchat plugin - Handle all the weirdnesses of the IRC-to-ChatterBox bridge that is FreeNode's #cbstream =head1 INSTALLATION First, install the required modules. (See the top of the file for the use statements.) Next, copy this file to your Xchat directory. For me, that is ~/.xchat2. YMMV, especially on Windows. The file should be named 'cbstream.pl', though only the extention is important. Then create a configuration file in the same directory called "cbstream.yaml". It should look like: --- cbstream: pmpassword: myreallylongpasswordthatIshouldneverforget pmuser: Tanktalus That's the minimal configuration required. Now load the plugin (I think this happens automatically on Xchat's start-up, but you can do it manually with the /load command). You're ready to go. =head1 CONFIGURATION OPTIONS Other than pmpassword and pmuser, other options include: =over 4 =item nick-prefix =item nick-suffix Here you can customise how the nicks will appear in Xchat. I set these this way: cbstream: nick-prefix: "[" nick-suffix: "]" Then nicks will show up like "[tye]" instead of just "tye". This makes it easier for cut&paste for me. =item ignore_mode If you have anyone being ignored, you can actually be told when you receive a message from them. You can set this mode to C to get a notice that a message has been ignored (and from whom), or C to not only get the notice, but the entire message they sent. =back =head1 WHAT IT DOES By default, it replaces the nick 'cbstream' on the left of the line in Xchat with the real nick (see nick-prefix and nick-suffix above). It also enables some new commands: =head1 COMMANDS =over 4 =item cblog This will do a quick login with the cbstream server, joining the #cbstream-login channel, posting your plogin command, and then closing the tabs. You may not end up back in the channel you started from. You only need to do this whenever cbstream is restarted, not just any time you join IRC. You still need to be registered with FreeNode's NickServ. =item cbig This will reload the /ignore's from PerlMonks to be used in IRC. Transfering back and forth is NOT automatic, and would cause undue stress on the PM server, slowing down your IRC experience. When you run this command, your system will retrieve your ignore list from Perlmonks, and then resolve each user number to a name. This lookup will use a local cache to avoid hitting the server too much for each number to name lookup. This also means that if you unignore on Perlmonks, the user may still show up in the cbstream->ignore->byid fields in your conf file. This is normal and won't interfere with anything. =item cbignore This will add an ignore, and save it to your PM user for use with other CB applications (such as Full Page Chat). =item cbunignore This will remove an ignore, and remove it from your PM user as well. =item cbmsg This will send a /msg to whomever you specify. Any private response will not be seen in IRC. =item cbset This will set a configuration value. Note that there's no validation here: you can set anything you want. /cbset foo bar This will persist via the config file for future use. =item cbget This will retrieve a configuration value from the config file. =item cbrm This will remove a configuration key from the config file. =back =head1 TODO * validation for cbset - ensuring you're setting real keys to allowed values =cut { use File::Spec; my $conf; my $conf_file = File::Spec->catfile(Xchat::get_info('xchatdir'), $name . '.yaml'); sub _conf { $conf ||= LoadFile($conf_file); $conf } sub get_conf { my @var = @_; Dive(_conf, @var); } sub set_conf { my $val = pop; DiveVal(_conf, @_) = $val; } sub rm_conf { my $val = pop; my $rm = Dive(_conf, @_); if ($rm) { delete $rm->{$val}; } } sub save_conf { DumpFile($conf_file, $conf) if $conf; } } Xchat::register($name, $version); Xchat::hook_command('cblog', sub { Xchat::command 'join #cbstream-login'; Xchat::EAT_ALL }); Xchat::hook_print('You Join', \&JoinCBStreamLogin); Xchat::hook_server('PRIVMSG', \&LeaveCBStreamLogin); # once logged in, tell cbstream what our id and pw is. sub JoinCBStreamLogin { my $info = shift; if (Xchat::get_info('network') eq 'FreeNode') { if ($info->[1] eq '#cbstream-login') { my $id = get_conf('cbstream','pmuser'); my $pw = get_conf('cbstream','pmpassword'); if ($id and $pw) { Xchat::set_context('#cbstream-login'); Xchat::command("say plogin $id $pw"); } return Xchat::EAT_NONE; } } Xchat::EAT_NONE; } sub LeaveCBStreamLogin { my $msg = shift; my $nth = shift; if (Xchat::get_info('network') eq 'FreeNode') { if ($msg->[0] =~ /^:cbstream!/ and $msg->[1] eq 'PRIVMSG' and lc $msg->[2] eq lc Xchat::get_info('nick') ) { if ($nth->[3] =~ /You are now persistently logged in as perlmonks user/) { Xchat::set_context('cbstream'); Xchat::command('close'); Xchat::set_context('#cbstream-login'); Xchat::command('close'); Xchat::set_context('#cbstream'); (my $realmsg = $nth->[3]) =~ s/^:\+//; Xchat::print("CBLOGIN: $realmsg"); return Xchat::EAT_ALL; } else { Xchat::set_context('#cbstream'); (my $realmsg = $nth->[3]) =~ s/^:\+//; Xchat::print("CB: $realmsg"); return Xchat::EAT_ALL; } } } return Xchat::EAT_NONE; } # check for ignored users... Xchat::hook_command('cbig', \&get_ignored); sub get_ignored { Xchat::print("Gathering ignores from Perlmonks"); rm_conf(qw(cbstream ignore byname)); require URI::Escape; my $user = URI::Escape::uri_escape(get_conf('cbstream', 'pmuser')); my $pass = URI::Escape::uri_escape(get_conf('cbstream', 'pmpassword')); my $me = LWP::Simple::get("http://www.perlmonks.org/index.pl?op=login;user=$user;passwd=$pass;displaytype=xml;ticker=yes;node=$user"); my $twig = XML::Twig->new(); $twig->parse($me); my @elt = $twig->get_xpath('//var[@name="ignoredusers"]'); if (@elt) { my $ignored = $elt[0]->text(); my @uids = ($ignored =~ /\|(\d+),/g); my @users = map { my $nick = get_conf(qw(cbstream ignore byid), $_) || do { my $xml = LWP::Simple::get("http://www.perlmonks.org/index.pl?displaytype=xml;node_id=$_"); my $nicktwig = XML::Twig->new(); $nicktwig->parse($xml); (my $user = ($nicktwig->get_xpath('//author'))[0]->text()) =~ s/^\s+//; $user =~ s/\s+$//; set_conf(qw(cbstream ignore byid), \$_, $user); $user; }; set_conf(qw(cbstream ignore byname), $nick, $_); $nick; } @uids; if (@users) { Xchat::print("Ignoring: @users"); } else { Xchat::print("Ignoring no one"); } save_conf(); } else { Xchat::print "no one being ignored (maybe failed to log in?)"; } Xchat::EAT_ALL; } Xchat::hook_command('cbignore', \&add_ignore); sub add_ignore { my $person = $_[1][1]; return Xchat::EAT_ALL unless $person; $person =~ s/^\[(.*)\]/$1/; Xchat::print "Adding [$person] to PM ignores"; Xchat::command "say /ignore [$person]"; set_conf(qw(cbstream ignore byname), $person, -1); Xchat::EAT_ALL; } Xchat::hook_command('cbunignore', \&rm_ignore); sub rm_ignore { my $person = $_[1][1]; return Xchat::EAT_ALL unless $person; $person =~ s/^\[(.*)\]/$1/; Xchat::print "Removing [$person] from PM ignores"; Xchat::command "say /unignore [$person]"; rm_conf(qw(cbstream ignore byname), $person); Xchat::EAT_ALL; } Xchat::hook_command('cbmsg', \&cb_msg); sub cb_msg { my $msg = shift; my $nth = shift; Xchat::print "Sending $nth->[1] (don't expect a response here)"; Xchat::command "say /msg $nth->[1]"; Xchat::EAT_ALL; } Xchat::hook_server('PRIVMSG', \&rewrite_cb, { priority => Xchat::PRI_LOW }); sub rewrite_cb { my $msg = shift; my $nth = shift; if (Xchat::get_info('network') eq 'FreeNode') { if ($msg->[0] =~ /^:cbstream!/ and $msg->[1] eq 'PRIVMSG' and $msg->[2] eq '#cbstream' and 1 ) { my $prefix = get_conf('cbstream','nick-prefix') || ''; my $suffix = get_conf('cbstream','nick-suffix') || ''; (my $alias) = $nth->[3] =~ /^\S*\[(.*?)\]/; (my $safealias = $alias) =~ s/\s/_/g; (my $newmsg = $nth->[0]) =~ s/^:cbstream!/:$prefix$safealias$suffix!/; $newmsg =~ s/\[\Q$alias\E\]\s+//; # check if it's someone we think we're ignoring... if (! get_conf(qw(cbstream ignore byname), $alias) ) { # if it's an action, we need to convert it to such. $newmsg =~ s[:\+/me (.*)][:\001ACTION $1\001]; Xchat::command("recv $newmsg"); } elsif (my $mode = get_conf(qw(cbstream ignore_mode))) { if ($mode eq '1' or $mode eq 'brief') { Xchat::print("$alias said something, but we're ignoring them."); } elsif ($mode eq '2' or $mode eq 'verbose') { $newmsg =~ s[^.*:\+][]; Xchat::print("$alias said '$newmsg', but we're ignoring them."); } } Xchat::EAT_ALL; } } } Xchat::hook_command('cbset', \&cb_set); sub cb_set { my $msg = shift; my $nth = shift; set_conf('cbstream', @{$msg}[1..$#$msg]); Xchat::print ("set " . join(' ', map { "[$_]" } @{$msg}[1..$#$msg-1]) . " to {" . $msg->[-1] . "}"); save_conf(); Xchat::EAT_ALL; } Xchat::hook_command('cbget', \&cb_get); sub cb_get { my $msg = shift; my $nth = shift; my $val = get_conf('cbstream', @{$msg}[1..$#$msg]); if (defined $val) { if ($nth->[1] =~ /password/) { $val = '(not displayed)'; } else { $val = "{ $val }" } } else { $val = ''; } Xchat::print(join(' ', map { "[$_]" } @{$msg}[1..$#$msg]) . " = $val"); Xchat::EAT_ALL; } Xchat::hook_command('cbrm', \&cb_rm); sub cb_rm { my $msg = shift; my $nth = shift; rm_conf('cbstream', @{$msg}[1..$#$msg]); Xchat::print('deleted ' . join(' ', map { "[$_]" } @{$msg}[1..$#$msg])); save_conf(); Xchat::EAT_ALL; } #### --- cbstream: pmuser: Tanktalus pmpassword: somelongpasswordthatI'llneverforgetIhope #### nick-prefix: "[" nick-suffix: "]"