<?xml version="1.0" encoding="windows-1252"?>
<node id="85317" title="pmchat" created="2001-06-03 15:28:38" updated="2005-08-14 20:59:15">
<type id="1748">
sourcecode</type>
<author id="47599">
mr.nick</author>
<data>
<field name="doctext">
&lt;code&gt;#!/usr/bin/perl -w 

##  
## pmchat by Nicholas J. Leon ala mr.nick (nicholas@binary9.net) 
##                                    http://www.mrnick.binary9.net 
##
## A text mode client for the Chatter Box of Perl Monks 
## This is not an attempt to be complete, but small and useful 
## Use it or not. No guaranteee, no warranty, blah blah 

## now features a debugging mode! Guaranteed to piss off less
## CB users than before!


my $ID='$Id: pmchat,v 1.65 2001/08/07 01:02:15 nicholas Exp $'; #'

use strict;
use XML::Simple;
use LWP::Simple; 
use LWP::UserAgent; 
use HTTP::Cookies; 
use HTTP::Request::Common; 
use Data::Dumper; 
use Text::Wrap qw($columns wrap); 
use Term::ReadLine; 
use Term::ReadKey qw(GetTerminalSize ReadMode ReadLine); 
use HTML::Parser;
use File::Copy;
use Storable;
use MD5;
use URI::Escape;
use HTML::Parser;
 
$|++; 

my $pm='http://www.perlmonks.org/index.pl'; 
my $cookie="$ENV{HOME}/.pmcookie"; 
my $cffile="$ENV{HOME}/.pmconfig"; 
my %config=( 
            timestamp =&gt; 0, 
            colorize =&gt; $^O=~/win/i ? 0 : 1, 
	    browser =&gt; '/usr/bin/lynx %s',
	    newnodes =&gt; 25,
	    updateonlaunch =&gt; 0,
	    timeout =&gt; 45,
	    away =&gt; 0,
	    debug =&gt; 0,
	    logfile =&gt; 'none',
           ); 
 
my %seenmsg; 
my %seenprv; 
my %xp;
my $ua;
 
## some color stuff (if you want) 
my %colormap= 
  (  
   node =&gt; [ "\e[33m", "\e[0m" ], 
   user =&gt; [ "\e[1m", "\e[0m" ], 
   code =&gt; [ "\e[32m", "\e[0m" ], 
   me =&gt; [ "\e[36m", "\e[0m" ], 
   private =&gt; [ "\e[35m","\e[0m" ],
   important =&gt; [ "\e[1;34m","\e[0m" ],
  ); 

## &lt;readmore&gt;
##############################################################################
##############################################################################

sub writeconfig { 
  store \%config,$cffile;
} 
sub readconfig { 
  %config=(%config,%{ retrieve $cffile }) if -f $cffile;

  ## away is ALWAYS unset
  $config{away}=0;
} 

sub autoupdate {
  my $quiet=shift;
  my $r=$ua-&gt;request(GET "http://www.mrnick.binary9.net/pmchat/version");

  if ($r-&gt;{_rc} != 200) {
    print "Sorry, update request failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
    return;
  }

  my($ver)=$r-&gt;content=~/^([\d\.]+)$/;
  my($this)=$ID=~/,v\s+([\d\.]+)/;
  

  print "This version is $this, the current version is $ver.\n" unless $quiet;

  if ($this &gt;= $ver) {
    print "There is no need to update.\n" unless $quiet;
    return;
  }

  print "A new version is available, $ver.\n";

  $r=$ua-&gt;request(GET "http://www.mrnick.binary9.net/pmchat/pmchat");

  if ($r-&gt;{_rc} != 200) {
    print "Sorry, update request failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
    return;
  }

  my $tmp=$ENV{TMP} || $ENV{TEMP} || "/tmp";
  my $fn="$tmp/pmchat-$ver";

  unless (open (OUT,"&gt;$fn")) {
    print "Unable to save newest version to $fn\n";
    return;
  }

  print OUT $r-&gt;content;
  close OUT;

  ## okay, a couple checks here: we can autoupdate IF the following
  ## are true
  if ($^O=~/win32/i) {
    print "Sorry, autoupdate not available for Windows installations.\n";
    print "The newest version has been saved in $fn.\n";
    return;
  }

  ## moving the old version someplace else 
  if (!move($0,"$0.bak")) {
    print "Couldn't move $0 to $0.bak, aborting.\n";
    print "The newest version has been saved in $fn.\n";
    return;
  }
  ## moving the new version to the old's location
  if (!move($fn,$0)) {
    print "Couldn't move $fn to $0, aborting $!.\n";
    move("$0.bak",$0);
    print "The newest version has been saved in $fn.\n";
    return;
  }
  ## okay! Reload!
  chmod 0755,$0;
  reload();
}


##############################################################################
##############################################################################
sub xml {
  my $r=shift;
  my $xml=$r-&gt;content;
  $xml=~ tr/\x80-\xff/\?/;
  $xml;
}

sub colorize {
  my $txt=shift;
  my $type=shift;

  return $txt unless $config{colorize};

  "$colormap{$type}[0]$txt$colormap{$type}[1]";
}

my %usermap;
my @colors=(31..36,41..46);

sub user {
  ## see if this user has b
  colorize(shift,"user");
}
sub imp {
  colorize(shift,"important");
}  
sub content {
  my $txt=shift;

  return $txt unless $config{colorize};

  unless ($txt=~s/\&lt;code\&gt;(.*)\&lt;\/code\&gt;/$colormap{code}[0]$1$colormap{code}[1]/mig) {
    $txt=~s/\[([^\]]+)\]/$colormap{node}[0]$1$colormap{node}[1]/g;
  }

  $txt;
}
##############################################################################
##############################################################################

sub cookie {
  $ua-&gt;cookie_jar(HTTP::Cookies-&gt;new());
  $ua-&gt;cookie_jar-&gt;load($cookie);
}

sub login {
  my $user; 
  my $pass; 
  
  ## fixed &lt;&gt; to &lt;STDIN&gt; via merlyn
  print "Enter your username: "; chomp($user=&lt;STDIN&gt;); 
  ReadMode 2;
  print "Enter your password: "; chomp($pass=&lt;STDIN&gt;); 
  ReadMode 0;
  print "\n";
  
  $ua-&gt;cookie_jar(HTTP::Cookies-&gt;new(file =&gt; $cookie, 
				     ignore_discard =&gt; 1, 
				     autosave =&gt; 1, 
				    ) 
		 ); 
  
  my $r=$ua-&gt;request( POST ($pm,[  
				 op=&gt; 'login',  
				 user=&gt; $user,  
				 passwd =&gt; $pass, 
				 expires =&gt; '+1y',  
				 node_id =&gt; '16046'  
				])); 

  if ($r-&gt;{_rc} != 200) {
    print "Sorry, login request failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
    return;
  }

}

sub xp { 
    my $r=$ua-&gt;request(GET("$pm?node_id=16046")); 

    if ($r-&gt;{_rc} != 200) {
      print "Sorry, XP request failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
      return;
    }

    my $xml=XMLin(xml($r));

    $config{xp}=$xml-&gt;{XP}-&gt;{xp} unless defined $config{xp};
    $config{level}=$xml-&gt;{XP}-&gt;{level} unless defined $config{level};


    print "\nYou are logged in as ".user($xml-&gt;{INFO}-&gt;{foruser}).".\n"; 
    print "You are level $xml-&gt;{XP}-&gt;{level} ($xml-&gt;{XP}-&gt;{xp} XP).\n"; 
    if ($xml-&gt;{XP}-&gt;{level} &gt; $config{level}) {
      print imp "You have gained a level!\n";
    }
    print "You have $xml-&gt;{XP}-&gt;{xp2nextlevel} XP left until the next level.\n"; 

    if ($xml-&gt;{XP}-&gt;{xp} &gt; $config{xp}) {
      print imp "You have gained ".($xml-&gt;{XP}-&gt;{xp} - $config{xp})." experience!\n";
    }
    elsif ($xml-&gt;{XP}-&gt;{xp} &lt; $config{xp}) { 
      print imp "You have lost ".($xml-&gt;{XP}-&gt;{xp} - $config{xp})." experience!\n"; 
    }                               

    ($config{xp},$config{level})=($xml-&gt;{XP}-&gt;{xp},$xml-&gt;{XP}-&gt;{level});

    print "\n"; 
  } 
 
sub who { 
  my $r=$ua-&gt;request(GET("$pm?node_id=15851"));

  if ($r-&gt;{_rc} != 200) {
    print "Sorry, who request failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
    return;
  }
  
  my $ref=XMLin(xml($r),forcearray=&gt;1); 
 
  print "\nUsers current online (";
  print $#{$ref-&gt;{user}} + 1;
  print "):\n";

  print wrap "\t","\t",map { $_-&gt;{username}." " } @{$ref-&gt;{user}};

  print "\n";
} 
 
sub newnodes { 
  my $r=$ua-&gt;request(GET("$pm?node_id=30175")); 
  if ($r-&gt;{_rc} != 200) {
    print "Sorry, newnodes request failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
    return;
  }
  my $ref=XMLin(xml($r),forcearray=&gt;1); 
  my $cnt=1; 
  my %users=map { ($_-&gt;{node_id},$_-&gt;{content}) } @{$ref-&gt;{AUTHOR}}; 
  
  print "\nNew Nodes:\n";
  
  if ($ref-&gt;{NODE}) {
    for my $x (sort { $b-&gt;{createtime} &lt;=&gt; $a-&gt;{createtime} } @{$ref-&gt;{NODE}}) { 
      print wrap "\t","\t\t", 
      sprintf("%d. [%d] %s by %s (%s)\n",$cnt,
	      $x-&gt;{node_id},$x-&gt;{content},
	      user(defined $users{$x-&gt;{author_user}} ? $users{$x-&gt;{author_user}}:"Anonymous Monk"),
	      $x-&gt;{nodetype});
      last if $cnt++==$config{newnodes}; 
    } 
  }
  print "\n";
  
} 

sub nodeinfo {
  my $r=$ua-&gt;request(GET "$pm?node_id=32704");
  if ($r-&gt;{_rc} != 200) {
    print "Sorry, node info failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
    return;
  }
  my $ref=XMLin(xml($r),forcearray=&gt;1); 

  $config{nodes}=$ref-&gt;{NODE} unless defined $config{nodes};

  if (defined $ref-&gt;{NODE}) { 
    for my $id (keys %{$ref-&gt;{NODE}}) { 
      $config{nodes}-&gt;{$id}-&gt;{reputation}=0 if ! defined $config{nodes}-&gt;{$id}-&gt;{reputation};

      my $ch=$ref-&gt;{NODE}-&gt;{$id}-&gt;{reputation}-$config{nodes}-&gt;{$id}-&gt;{reputation};

      if ($ch) {
	print wrap "\t","\t\t","$ref-&gt;{NODE}-&gt;{$id}-&gt;{content} ($id) has ";
	print imp (($ch&gt;0?"gained":"lost")." $ch ");
	print "reputation!\n";

	$config{nodes}-&gt;{$id}-&gt;{reputation}=$ref-&gt;{NODE}-&gt;{$id}-&gt;{reputation};
      }
    }
    print "\n";
  }
}

sub getnode {
  my $id=shift;

  system(sprintf($config{browser},"$pm?node_id=$id"));
}

sub quit {
  writeconfig;
  exit;
}

sub set {
  my $args=shift;

  if ($args) {
    if ($args=~/([^\s]+)\s+(.+)$/) {
      $config{$1}=$2;
      print "\t$1 is now $2\n";
    }
    elsif ($args=~/([^\s+]+)$/) {
      print "\t$1 is $config{$1}\n";
    }
  }
  else {
    for my $k (sort keys %config) {
      next if ref $config{$k};
      printf "\t%-15s %s\n",$k,$config{$k};
    }
  }
}

sub reload {
  print "Reloading $0...\n";
  writeconfig;
  exec $0;
}

sub away {
  my $args=shift;

  print wrap '','',"You are now away. Checking your XP or sending a message will negate this.\n";

  $config{away}=1;
}

sub logfile {
  my $buff=shift;

  if ($config{logfile} &amp;&amp; $config{logfile} ne '0' &amp;&amp; $config{logfile} ne 'none') {
    if (!open(OUT,"&gt;&gt;$config{logfile}")) {
      warn "Couldn't open log file '$config{logfile}': $!\n";
      return;
    }
    print OUT $buff,"\n";
    close OUT;
  }
}

##############################################################################
##############################################################################

sub showmessage {
  my $msg=shift;
  my $type=shift || '';
  
  for my $k (keys %$msg) {
    $msg-&gt;{$k}=~s/^\s+|\s+$//g
  }

  print "\r";

  my $content=$msg-&gt;{content};
  
  if ($type eq 'private') {
    print wrap('',"\t",
	       ($config{timestamp}?sprintf "%02d:%02d:%02d/",(unpack("A8A2A2A2",$msg-&gt;{time}))[1..3]:'').
	       colorize("$msg-&gt;{author} says $msg-&gt;{content}","private").
	       "\n");
    logfile (($config{timestamp}?sprintf "%02d:%02d:%02d/",(unpack("A8A2A2A2",$msg-&gt;{time}))[1..3]:'').
      "$msg-&gt;{author} says $msg-&gt;{content}");
  }
  else {
    if ($msg-&gt;{content}=~s/^\/me\s+//) {
      print wrap('',"\t",
		 ($config{timestamp}?sprintf "%02d:%02d:%02d/",(unpack("A8A2A2A2",$msg-&gt;{time}))[1..3]:'').
		 colorize("$msg-&gt;{author} $msg-&gt;{content}","me"),
		 "\n");
      logfile (($config{timestamp}?sprintf "%02d:%02d:%02d/",(unpack("A8A2A2A2",$msg-&gt;{time}))[1..3]:'').
	"$msg-&gt;{author} $msg-&gt;{content}");
    }
    else {
    
      print wrap('',"\t",
		 ($config{timestamp}?sprintf "%02d:%02d:%02d/",(unpack("A8A2A2A2",$msg-&gt;{time}))[1..3]:'').
		 colorize($msg-&gt;{author},"user").
		 ": ".
		 content($msg-&gt;{content}).
		 "\n");
      logfile (($config{timestamp}?sprintf "%02d:%02d:%02d/",(unpack("A8A2A2A2",$msg-&gt;{time}))[1..3]:'').
	"$msg-&gt;{author}: $msg-&gt;{content}");
    }
  }
}
	     

sub getmessages { 
  my $r;
  ## alright, something wacky here. If $config{away} is true, then
  ## don't use the users cookie to grab the list
  if ($config{away}) {
    my $nua=LWP::UserAgent-&gt;new;
    $nua-&gt;agent("pmchat-mrnick-anon"); 
    $r=$nua-&gt;request(GET("$pm?node_id=15834"));  
  }
  else {
    $r=$ua-&gt;request(GET("$pm?node_id=15834")); 
  }
  if ($r-&gt;{_rc} != 200) {
    print "Sorry, message request failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
    return;
  }

  ## we'll cheese-out here ... for XML::Simple
  my $xml=xml($r);

  my $ref=XMLin(uri_escape($xml,"\x80-\xff"), forcearray=&gt;1 ); 
  
  if (defined $ref-&gt;{message}) { 
    for my $mess (@{$ref-&gt;{message}}) { 
      ## ignore this message if we've already printed it out 
      next if $seenmsg{"$mess-&gt;{user_id}:$mess-&gt;{time}"}++; 

      showmessage $mess; 
    } 
  } 
  else { 
    ## if there is nothing in the list, reset ours 
    undef %seenmsg; 
  } 
} 

sub getprivatemessages { 
  my $r=$ua-&gt;request(GET("$pm?node_id=15848")); 

  if ($r-&gt;{_rc} != 200) {
    print "Sorry, private message request failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
    return;
  }
  my $ref=XMLin(xml($r),forcearray=&gt;1); 
  
  if (defined $ref-&gt;{message}) { 
    for my $mess (@{$ref-&gt;{message}}) { 
      ## ignore this message if we've already printed it out 
      next if $seenprv{"$mess-&gt;{user_id}:$mess-&gt;{time}"}++; 
 
      showmessage $mess,"private"; 
    } 
  } 
  else { 
    undef %seenprv; 
  } 
} 

sub postmessage { 
  my $junk=shift;
  my $msg=shift; 

  if ($config{debug}) {
    print "&gt;&gt; $msg\n";
    return;
  }

  my $req=POST ($pm,[ 
                     op=&gt;'message', 
                     message=&gt;$msg, 
                     node_id=&gt;'16046', 
                    ]); 
  
  my $r=$ua-&gt;request($req); 

  if ($r-&gt;{_rc} != 200) {
    print "Sorry, post message failed: $r-&gt;{_rc}/$r-&gt;{_msg}\n";
    return;
  }

} 

sub help {
  print &lt;&lt;EOT
The following commands are available:
    /away         :: Sets pmchat to anonymously pull Chatterbox messages. The
                     effect is that you will not appear in Other Users unless
                     you send a message or check your XP.
    /help         :: Shows this message
    /getnode ID   :: Retrieves the passed node and launches your user
                     configurable browser ("browser") to view that node.
    /newnodes     :: Displays a list of the newest nodes (of all types)
                     posted. The number of nodes displayed is limited by
                     the "newnodes" user configurable variable.
    /nodeinfo     :: Displays changes in reputation for your nodes.
    /reload       :: UNIX ONLY. Restarts pmchat.
    /set          :: Displays a list of all the user configurable
                     variables and their values.
    /set X Y      :: Sets the user configurable variable X to
                     value Y.
    /update       :: Checks for a new version of pmchat, and if it
                     exists, download it.
                     This WILL overwrite your current version.
    /quit         :: Exits pmchat
    /who          :: Shows a list of all users currently online
    /xp           :: Shows your current experience and level.
EOT
  ;
}

##############################################################################
##############################################################################
my $old;
my $term=new Term::ReadLine 'pmchat';

sub getlineUnix {
  my $message;

  eval {
    local $SIG{ALRM}=sub { 
      $old=$readline::line; 
      die 
    };
    
    ## I don't use the version of readline from ReadKey (that includes a timeout)
    ## because this version stores the interrupted (what was already typed when the
    ## alarm() went off) text in a variable. I need that so I can restuff it 
    ## back in.

    alarm($config{timeout}) unless $^O=~/win32/i;
    $message=$term-&gt;readline("(Talk) ",$old);
    $old=$readline::line='';
    alarm(0) unless $^O=~/win32/i;
  };    

  $message;
}

sub getlineWin32 {
  ## sorry, non-blocking reads are not supported on Windows, it appears
  print "(Talk) ";
  chomp($_=&lt;STDIN&gt;);
  $_;
}

## initialize our user agent
$ua=LWP::UserAgent-&gt;new || die "Couldn't init UserAgent: $!\n";
$ua-&gt;agent("pmchat-mrnick"); 

## trap ^C's
## for clean exit
$SIG{INT}=sub { 
  writeconfig;
  exit 
};

## load up our config defaults
readconfig;

## for text wrapping
$columns=(Term::ReadKey::GetTerminalSize)[0] || $ENV{COLS} || $ENV{COLUMNS} || 80;

if (-e $cookie) {
  cookie;
}
else {
  login;
}

my($this)=$ID=~/,v\s+([\d\.]+)/;

print "This is pmchat version $this.\n";

if ($config{updateonlaunch}) {
  autoupdate(1);
}
else {
  print "Consider checking for a new version with /update.\n";
}

xp();
nodeinfo();
print "Type /help for help.\n";
who();
newnodes();
getprivatemessages;
getmessages();

## testing, please ignore
my %cmdmap=(
	    '/me' =&gt; \&amp;postmessage,
	    '/msg' =&gt; \&amp;postmessage,

	    '/away', =&gt; \&amp;away,
	    '/who' =&gt; \&amp;who,
	    '/quit' =&gt; \&amp;quit,
	    '/set' =&gt; \&amp;set,
	    '/new\s*nodes' =&gt; \&amp;newnodes,
	    '/xp' =&gt; \&amp;xp,
	    '/getnode' =&gt; \&amp;getnode,
	    '/help' =&gt; \&amp;help,
	    '/reload' =&gt; \&amp;reload,
	    '/update' =&gt; \&amp;autoupdate,
	    '/nodeinfo' =&gt; \&amp;nodeinfo,
	   );


while (1) {
  my $message;
  
  getprivatemessages unless $config{away};
  getmessages;
  
  if ($^O=~/win32/i) {
    $message=getlineWin32;
  }
  else {
    $message=getlineUnix;
  }
  
  if (defined $message) {
    if ($message=~/^\//) {
      foreach (keys %cmdmap) {
	if ($message=~/^$_\s*(.*)/) {
	  &amp;{$cmdmap{$_}}($1,$message);
	  last;
	}
      }
    }
    else {
      postmessage undef,$message;
    }
  }
}
&lt;/code&gt;</field>
<field name="codedescription">
A text-mode client for the Chatterbox.
&lt;p&gt;
Main features:
&lt;ul&gt;
&lt;li&gt;Colorized output via ANSI escape sequences
&lt;li&gt;Compatible with both *nix and Win32 (mostly :)
&lt;li&gt;Displays most recent [New Nodes]
&lt;li&gt;Can launch a browser to view a node
&lt;li&gt;Check for and autoinstall updates
&lt;li&gt;An "Away" mode for not being in "Other Users"
&lt;li&gt;Uses the various [XML Generators] of PerlMonks
&lt;li&gt;Tracks XP status
&lt;li&gt;Tracks Reputation of your nodes (and displays WHICH nodes have changed -- and by how much)
&lt;li&gt;Requires NO configuration, download and run!
&lt;li&gt;Displays Other Users

&lt;li&gt;User configurable options (inside Pmchat) for
 &lt;ul&gt;
 &lt;li&gt;Time-stamping each message (for long-term running)
 &lt;li&gt;Turn off/on colorization
 &lt;li&gt;Number of [New Nodes] listed
 &lt;li&gt;Which browser to use for node viewing
 &lt;li&gt;Update on Launch
 &lt;li&gt;Seconds between polls for new messages (n/a on Windows)
 &lt;li&gt;Debugging mode. No output is sent to Perlmonks
 &lt;li&gt;Log file. Keeps a log of all messages. Set "logfile" to the desired filename. To stop, set "logfile" to "0" or "none".
 &lt;/ul&gt;
&lt;/ul&gt;
&lt;p&gt;
Because [pmchat] is a text-mode client, it has the following shortcomings
&lt;ul&gt;
&lt;li&gt;Doesn't render HTML tags
&lt;li&gt;Doesn't render special HTML entities (eg: &amp;amp;amp;)
&lt;/ul&gt;
</field>
<field name="codecategory">
PerlMonks.org Related Scripts</field>
<field name="codeauthor">
mr.nick @ Perlmonks.org (email: mrnick@binary9.net)</field>
</data>
</node>
