http://qs321.pair.com?node_id=82809

   1: #!/usr/bin/perl -w 
   2: 
   3:  
   4: ##  
   5: ## pmchat by Nicholas J. Leon ala mr.nick (nicholas@binary9.net) 
   6: ##                                    http://www.mrnick.binary9.net 
   7: 
   8: ## A text mode client for the Chatter Box of Perl Monks 
   9: ## this is not an attempt to be complete, but small and useful 
  10: ## Use it or not. No guaranteee, no warranty, blah blah 
  11: 
  12: ## Now supports Win32 installations with a different ReadLine
  13: ## call.
  14: 
  15: ## Autoupdate now actually autoupdates
  16: 
  17: ## Oh, and it has no error checking :) 
  18: 
  19: 
  20: my $ID='$Id: pmchat,v 1.42 2001/06/03 17:49:22 nicholas Exp $'; #'
  21:  
  22: use strict; 
  23: use XML::Simple; 
  24: use LWP::Simple; 
  25: use LWP::UserAgent; 
  26: use HTTP::Cookies; 
  27: use HTTP::Request::Common; 
  28: use Data::Dumper; 
  29: use Text::Wrap qw($columns wrap); 
  30: use Term::ReadLine; 
  31: use Term::ReadKey qw(GetTerminalSize); 
  32: use HTML::Parser;
  33: use File::Copy;
  34:  
  35: $|++; 
  36: 
  37: my $pm='http://www.perlmonks.org/index.pl'; 
  38: my $cookie="$ENV{HOME}/.pmcookie"; 
  39: my $cffile="$ENV{HOME}/.pmconfig"; 
  40: my %config=( 
  41:             timestamp => 0, 
  42:             colorize => 1, 
  43:             browser => '/usr/bin/lynx %s',
  44:             newnodes => 25,
  45:             updateonlaunch => 0,
  46:             timeout => 15,
  47:            ); 
  48:  
  49: my %seenmsg; 
  50: my %seenprv; 
  51: my %xp;
  52: my $ua;
  53:  
  54: ## some color stuff (if you want) 
  55: my %colormap= 
  56:   (  
  57:    node => [ "\e[33m", "\e[0m" ], 
  58:    user => [ "\e[1m", "\e[0m" ], 
  59:    code => [ "\e[32m", "\e[0m" ], 
  60:    me => [ "\e[36m", "\e[0m" ], 
  61:    private => [ "\e[35m","\e[0m" ],
  62:    important => [ "\e[1;34m","\e[0m" ],
  63:   ); 
  64: 
  65: ## 
  66: ##############################################################################
  67: ##############################################################################
  68: 
  69: sub writeconfig { 
  70:   unless (open(OUT,">$cffile")) { 
  71:     warn "Couldn't open '$cffile' for writing: $!\n"; 
  72:     return; 
  73:   } 
  74: 
  75:   print OUT "$_ $config{$_}\n" for keys %config; 
  76: 
  77:   close OUT; 
  78: } 
  79: sub readconfig { 
  80:   unless (open(IN,$cffile)) { 
  81:     warn "Couldn't open '$cffile' for reading: $!\n"; 
  82:     return; 
  83:   } 
  84:   
  85:   %config=(%config,(map /^([^\s]+)\s+(.+)$/,<IN>));
  86:   
  87:   close IN; 
  88: } 
  89: 
  90: ## testing ... autoupdate
  91: sub autoupdate {
  92:   my $quiet=shift;
  93:   my $r=$ua->request(GET "http://www.mrnick.binary9.net/pmchat/version");
  94:   my($ver)=$r->content=~/^([\d\.]+)$/;
  95:   my($this)=$ID=~/,v\s+([\d\.]+)/;
  96:   
  97:   print "This version is $this, the current version is $ver.\n" unless $quiet;
  98: 
  99:   if ($this >= $ver) {
 100:     print "There is no need to update.\n" unless $quiet;
 101:     return;
 102:   }
 103: 
 104:   print "A new version is available, $ver.\n";
 105: 
 106:   $r=$ua->request(GET "http://www.mrnick.binary9.net/pmchat/pmchat");
 107: 
 108:   my $tmp=$ENV{TMP} || $ENV{TEMP} || "/tmp";
 109:   my $fn="$tmp/pmchat-$ver";
 110: 
 111:   unless (open (OUT,">$fn")) {
 112:     print "Unable to save newest version to $fn\n";
 113:     return;
 114:   }
 115: 
 116:   print OUT $r->content;
 117:   close OUT;
 118: 
 119:   ## okay, a couple checks here: we can autoupdate IF the following
 120:   ## are true
 121:   if ($^O=~/win32/i) {
 122:     print "Sorry, autoupdate not available for Windows installations.\n";
 123:     print "The newest version has been saved in $tmp/pmchat.$ver.\n";
 124:     return;
 125:   }
 126: 
 127:   ## moving the old version someplace else 
 128:   if (!move($0,"$0.bak")) {
 129:     print "Couldn't move $0 to $0.bak, aborting.\n";
 130:     print "The newest version has been saved in $fn.\n";
 131:     return;
 132:   }
 133:   ## moving the new version to the old's location
 134:   if (!move($fn,$0)) {
 135:     print "Couldn't move $fn to $0, aborting $!.\n";
 136:     move("$0.bak",$0);
 137:     print "The newest version has been saved in $fn.\n";
 138:     return;
 139:   }
 140:   ## okay! Reload!
 141:   chmod 0755,$0;
 142:   writeconfig;
 143:   exec $0;
 144: }
 145:   
 146: 
 147: ##############################################################################
 148: ##############################################################################
 149: 
 150: sub colorize {
 151:   my $txt=shift;
 152:   my $type=shift;
 153: 
 154:   return $txt unless $config{colorize};
 155:   return $txt if $^O=~/win32/i;
 156: 
 157:   "$colormap{$type}[0]$txt$colormap{$type}[1]";
 158: }
 159: 
 160: sub user {
 161:   colorize(shift,"user");
 162: }
 163: sub imp {
 164:   colorize(shift,"important");
 165: }  
 166: sub content {
 167:   my $txt=shift;
 168: 
 169:   return $txt unless $config{colorize};
 170:   return $txt if $^O=~/win32/i;
 171: 
 172:   unless ($txt=~s/\<code\>(.*)\<\/code\>/$colormap{code}[0]$1$colormap{code}[1]/mig) {
 173:     $txt=~s/\[([^\]]+)\]/$colormap{node}[0]$1$colormap{node}[1]/g;
 174:   }
 175: 
 176:   $txt;
 177: }
 178: ##############################################################################
 179: ##############################################################################
 180: 
 181: sub cookie {
 182:   $ua->cookie_jar(HTTP::Cookies->new());
 183:   $ua->cookie_jar->load($cookie);
 184: }
 185: 
 186: sub login {
 187:   my $user; 
 188:   my $pass; 
 189:   
 190:   ## fixed <> to <STDIN> via merlyn
 191:   print "Enter your username: "; chomp($user=<STDIN>); 
 192:   print "Enter your password: "; chomp($pass=<STDIN>); 
 193:   
 194:   $ua->cookie_jar(HTTP::Cookies->new(file => $cookie, 
 195:                                      ignore_discard => 1, 
 196:                                      autosave => 1, 
 197:                                     ) 
 198:                  ); 
 199:   
 200:   my $r=$ua->request( POST ($pm,[  
 201:                                  op=> 'login',  
 202:                                  user=> $user,  
 203:                                  passwd => $pass, 
 204:                                  expires => '+1y',  
 205:                                  node_id => '16046'  
 206:                                 ])); 
 207: }
 208: 
 209: sub xp { 
 210:     my $r=$ua->request(GET("$pm?node_id=16046")); 
 211:     my $xml=XMLin($r->content); 
 212:     
 213:     $config{xp}=$xml->{XP}->{xp} unless defined $config{xp};
 214:     $config{level}=$xml->{XP}->{level} unless defined $config{level};
 215: 
 216: 
 217:     print "\nYou are logged in as ".user($xml->{INFO}->{foruser}).".\n"; 
 218:     print "You are level $xml->{XP}->{level} ($xml->{XP}->{xp} XP).\n"; 
 219:     if ($xml->{XP}->{level} > $config{level}) {
 220:       print imp "You have gained a level!\n";
 221:     }
 222:     print "You have $xml->{XP}->{xp2nextlevel} XP left until the next level.\n"; 
 223: 
 224:     if ($xml->{XP}->{xp} > $config{xp}) {
 225:       print imp "You have gained ".($xml->{XP}->{xp} - $config{xp})." experience!\n";
 226:     }
 227:     elsif ($xml->{XP}->{xp} < $config{xp}) { 
 228:       print imp "You have lost ".($xml->{XP}->{xp} - $config{xp})." experience!\n"; 
 229:     }                               
 230: 
 231:     ($config{xp},$config{level})=($xml->{XP}->{xp},$xml->{XP}->{level});
 232: 
 233:     print "\n"; 
 234:   } 
 235:  
 236: sub who { 
 237:   my $req=GET("$pm?node_id=15851"); 
 238:   my $res=$ua->request($req); 
 239:   my $ref=XMLin($res->content,forcearray=>1); 
 240:  
 241:   print "\nUsers current online (";
 242:   print $#{$ref->{user}} + 1;
 243:   print "):\n";
 244: 
 245:   print wrap "\t","\t",map { user($_->{username})." " } @{$ref->{user}};
 246: 
 247:   print "\n";
 248: } 
 249:  
 250: sub newnodes { 
 251:   my $req=GET("$pm?node_id=30175"); 
 252:   my $res=$ua->request($req); 
 253:   my $ref=XMLin($res->content,forcearray=>1); 
 254:   my $cnt=1; 
 255:   my %users=map { ($_->{node_id},$_->{content}) } @{$ref->{AUTHOR}}; 
 256:   
 257:   print "\nNew Nodes:\n";
 258:   
 259:   if ($ref->{NODE}) {
 260:     for my $x (sort { $b->{createtime} <=> $a->{createtime} } @{$ref->{NODE}}) { 
 261:       print wrap "\t","\t\t", 
 262:       sprintf("%d. [%d] %s by %s (%s)\n",$cnt,
 263:               $x->{node_id},$x->{content},
 264:               user(defined $users{$x->{author_user}} ? $users{$x->{author_user}}:"Anonymous Monk"),
 265:               $x->{nodetype});
 266:       last if $cnt++==$config{newnodes}; 
 267:     } 
 268:   }
 269:   print "\n";
 270:   
 271: } 
 272: 
 273: ##############################################################################
 274: ##############################################################################
 275: 
 276: sub showmessage {
 277:   my $msg=shift;
 278:   my $type=shift || '';
 279:   
 280:   for my $k (keys %$msg) {
 281:     $msg->{$k}=~s/^\s+|\s+$//g
 282:   }
 283: 
 284:   print "\r";
 285:   
 286:   if ($type eq 'private') {
 287:     print wrap('',"\t",
 288:                ($config{timestamp}?sprintf "%02d:%02d:%02d/",(unpack("A8A2A2A2",$msg->{time}))[1..3]:'').
 289:                colorize("$msg->{author} says $msg->{content}","private").
 290:                "\n");
 291:   }
 292:   else {
 293:     if ($msg->{content}=~s/^\/me\s+//) {
 294:       print wrap('',"\t",
 295:                  ($config{timestamp}?sprintf "%02d:%02d:%02d/",(unpack("A8A2A2A2",$msg->{time}))[1..3]:'').
 296:                  colorize("$msg->{author} $msg->{content}","me"),
 297:                  "\n");
 298:     }
 299:     else {
 300:       print wrap('',"\t",
 301:                  ($config{timestamp}?sprintf "%02d:%02d:%02d/",(unpack("A8A2A2A2",$msg->{time}))[1..3]:'').
 302:                  colorize($msg->{author},"user").
 303:                  ": ".
 304:                  content($msg->{content}).
 305:                  "\n");
 306:     }
 307:   }
 308: }
 309:              
 310: 
 311: sub getmessages { 
 312:   my $req=GET("$pm?node_id=15834"); 
 313:   my $res=$ua->request($req); 
 314:   my $ref=XMLin($res->content, forcearray=>1 ); 
 315:   
 316:   if (defined $ref->{message}) { 
 317:     for my $mess (@{$ref->{message}}) { 
 318:       ## ignore this message if we've already printed it out 
 319:       next if $seenmsg{"$mess->{user_id}:$mess->{time}"}++; 
 320: 
 321:       showmessage $mess; 
 322:     } 
 323:   } 
 324:   else { 
 325:     ## if there is nothing in the list, reset ours 
 326:     undef %seenmsg; 
 327:   } 
 328: } 
 329: 
 330: sub getprivatemessages { 
 331:   my $req=GET("$pm?node_id=15848"); 
 332:   my $res=$ua->request($req); 
 333:   my $ref=XMLin($res->content,forcearray=>1); 
 334:   
 335:   if (defined $ref->{message}) { 
 336:     for my $mess (@{$ref->{message}}) { 
 337:       ## ignore this message if we've already printed it out 
 338:       next if $seenprv{"$mess->{user_id}:$mess->{time}"}++; 
 339:  
 340:       showmessage $mess,"private"; 
 341:     } 
 342:   } 
 343:   else { 
 344:     undef %seenprv; 
 345:   } 
 346: } 
 347: 
 348: sub postmessage { 
 349:   my $msg=shift; 
 350:   my $req=POST ($pm,[ 
 351:                      op=>'message', 
 352:                      message=>$msg, 
 353:                      node_id=>'16046', 
 354:                     ]); 
 355:   
 356:   $ua->request($req); 
 357: } 
 358: 
 359: sub node {
 360:   my $id=shift;
 361: 
 362:   system(sprintf($config{browser},"$pm?node_id=$id"));
 363: }
 364: 
 365: sub help {
 366:   print <<EOT
 367: The following commands are available:
 368:     /help         :: Shows this message
 369:     /newnodes     :: Displays a list of the newest nodes (of all types)
 370:                      posted. The number of nodes displayed is limited by
 371:                      the "newnodes" user configurable variable.
 372:     /node ID      :: Retrieves the passed node and launches your user
 373:                      configurable browser ("browser") to view that node.
 374:     /reload       :: UNIX ONLY. Restarts pmchat.
 375:     /set          :: Displays a list of all the user configurable
 376:                      variables and their values.
 377:     /set X Y      :: Sets the user configurable variable X to
 378:                      value Y.
 379:     /update       :: Checks for a new version of pmchat, and if it
 380:                      exists, download it into a temporary location.
 381:                      This WILL NOT overwrite your current version.
 382:     /quit         :: Exits pmchat
 383:     /who          :: Shows a list of all users currently online
 384:     /xp           :: Shows your current experience and level.
 385: EOT
 386:   ;
 387: }
 388: 
 389: ##############################################################################
 390: ##############################################################################
 391: my $old;
 392: my $term=new Term::ReadLine 'pmchat';
 393: 
 394: sub getlineUnix {
 395:   my $message;
 396: 
 397:   eval {
 398:     local $SIG{ALRM}=sub { 
 399:       $old=$readline::line; 
 400:       die 
 401:     };
 402:     
 403:     ## I don't use the version of readline from ReadKey (that includes a timeout)
 404:     ## because this version stores the interrupted (what was already typed when the
 405:     ## alarm() went off) text in a variable. I need that so I can restuff it 
 406:     ## back in.
 407: 
 408:     alarm($config{timeout}) unless $^O=~/win32/i;
 409:     $message=$term->readline("Talk: ",$old);
 410:     $old=$readline::line='';
 411:     alarm(0) unless $^O=~/win32/i;
 412:   };    
 413: 
 414:   $message;
 415: }
 416: 
 417: sub getlineWin32 {
 418:   my $message=ReadLine($config{timeout});
 419: 
 420:   ## unfortunately, there is no way to preserve what was already typed
 421:   ## when the timeout occured. If you are typing when it happens,
 422:   ## you lose your text.
 423: 
 424:   $message;
 425: }
 426: 
 427: ## initialize our user agent
 428: $ua=LWP::UserAgent->new;
 429: $ua->agent("pmchat-mrnick"); 
 430: 
 431: ## trap ^C's
 432: ## for clean exit
 433: $SIG{INT}=sub { 
 434:   writeconfig;
 435:   exit 
 436: };
 437: 
 438: ## load up our config defaults
 439: readconfig;
 440: 
 441: ## for text wrapping
 442: $columns=(Term::ReadKey::GetTerminalSize)[0] || $ENV{COLS} || $ENV{COLUMNS} || 80;
 443: 
 444: if (-e $cookie) {
 445:   cookie;
 446: }
 447: else {
 448:   login;
 449: }
 450: 
 451: my($this)=$ID=~/,v\s+([\d\.]+)/;
 452: 
 453: print "This is pmchat version $this.\n";
 454: 
 455: autoupdate(1) if $config{updateonlaunch};
 456: xp();
 457: print "Type /help for help.\n";
 458: who();
 459: newnodes();
 460: getprivatemessages;
 461: getmessages();
 462: 
 463: 
 464: while (1) {
 465:   my $message;
 466: 
 467:   getprivatemessages;
 468:   getmessages;
 469:   
 470:   if ($^O=~/win32/i) {
 471:     $message=getlineWin32;
 472:   }
 473:   else {
 474:     $message=getlineUnix;
 475:   }
 476: 
 477:   if (defined $message) {
 478:     ## we understand a couple of commands
 479:     if ($message=~/^\/who/i) {
 480:       who;
 481:     }
 482:     elsif ($message=~/^\/quit/i) {
 483:       writeconfig;
 484:       exit;
 485:     }
 486:     elsif ($message=~/^\/set\s+([^\s]+)\s+(.+)$/) {
 487:       $config{$1}=$2;
 488:       print "$1 is now $2\n";
 489:     }
 490:     elsif ($message=~/^\/set$/) {
 491:       for my $k (sort keys %config) {
 492:         printf "\t%-10s %s\n",$k,$config{$k};
 493:       }
 494:     }
 495:     elsif ($message=~/^\/new\s*nodes/) {
 496:       newnodes;
 497:     }
 498:     elsif ($message=~/^\/xp/) {
 499:       xp;
 500:     }
 501:     elsif ($message=~/^\/node\s+(\d+)/) {
 502:       node($1);
 503:     }
 504:     elsif ($message=~/^\/help/) {
 505:       help;
 506:     }
 507:     elsif ($message=~/^\/reload/) {
 508:       print "Reloading $0!\n";
 509:       writeconfig;
 510:       exec $0;
 511:     }
 512:     elsif ($message=~/^\/update/) {
 513:       autoupdate;
 514:     }
 515:     elsif ($message=~/^\/me/ || $message=~/^\/msg/) {
 516:       postmessage($message);
 517:     }
 518:     elsif ($message=~/^\//) {
 519:       print "Unknown command '$message'.\n";
 520:     }
 521:     else {
 522:       postmessage($message);
 523:     }
 524:   }
 525: }