#!/usr/bin/perl # Webster 4.1.3 # Webster - a MUSH spellchecking robot. # Rewritten by Raevnos (Shawn Wagner - raevnos@pennmush.org) # Based off of Webster 3.4 by Javelin. require 5.005; # Make sure we have a fairly recent version of perl 5. # Declare global variables that are set in the section below. use vars qw($host $ispell_prog $local_dict $port $spelling_log $web_name $web_pass $DEBUG $DOLOG $dict_server $server_type); #################################################### # User-configurable stuff and run-time options # Most of these need to be set by you. #################################################### # Where is the MUSH? $host = 'localhost'; $port = 4201; # What's the char to log in to? $web_name = 'Webster'; $web_pass = 'webby'; # CHANGE THIS! # Where's the ispell program, the local dictionary, # and the place to log misspelled words? $ispell_prog = "ispell"; $local_dict = "local.dict"; # You might have to specify the full path to $spelling_log = "webspell.log"; # these files. # What DICT server do we use to define words? $dict_server = "dict.org"; # Setting this to 0 will disable logging. The $spelling_log file # can grow to be quite large - turn off this option if you're # cramped for disk space, or don't care to see what's marked # as bad words that shouldn't be for your MUSH. $DOLOG = 0; # Setting this to 1 will produce some extra log files. # Use only if you're having problems with getting Webster # to work. Otherwise, leave it set to 0. $DEBUG = 0; ############################################################# # End of user-configurable stuff # Try to avoid editing anything below this line. ############################################################# use strict; use Net::Telnet 3; # Some places have a old, different Net::Telnet. use Net::Dict; use IO::Handle; use IO::File; use IPC::Open2; use Text::ParseWords; use subs qw(spell spell_parse_line lookup error); # Predeclare subroutines. # Declare global variables used below that shouldn't be touched # by the user. use vars qw($pid $session $LOG $SPELL $TOSPELL $safe_web_pass); $TOSPELL = new IO::Handle; $SPELL = new IO::Handle; $LOG = new IO::File if $DOLOG; $server_type = "generic"; $SIG{CHLD} = \&REAPER; $SIG{USR1} = \&RELOAD; if ($DOLOG) { # Open up the log file. $LOG->open($spelling_log, "a") or die "Couldn't open $spelling_log: $!\n"; $LOG->autoflush(1); } # Open up a two-way pipe to the spelling program. $pid = open2 $SPELL, $TOSPELL, $ispell_prog, '-a', "-p$local_dict"; # Read in ispell's first line, which is just version info. $_ = <$SPELL>; warn "Starting $_" if $DEBUG; # Print it out, for debugging. # Escape regexp metacharacters from the password. $safe_web_pass = quotemeta $web_pass; # Start a big loop so that Webster keeps trying to reconnect to the # MUSH if it fails. while (1) { $session = new Net::Telnet(Host=>$host, Port=>$port, Timeout=>30, Errmode=>'return', Cmd_remove_mode=>0, Output_record_separator=>"\n", Input_record_separator=>"\n"); my $login_prompt = "WHO"; my $timeout= 1; my $shutdown = 0; my $counter = 0; if ($DEBUG) { $session->input_log("input.log"); $session->output_log("output.log"); $session->dump_log("dump.log"); } sleep 300, redo unless $session; # Get to the login prompt while (1) { $_ = $session->get(Timeout=>$timeout); $session->close, exit 0 if $session->eof; last if /$login_prompt/o; } # Log in and go home $session->print("connect $web_name $web_pass"); $session->print("home"); $session->print("think WEBSTERCONF: [version()]"); # Loop and wait for data read. If so, parse it. until ($session->eof) { $_ = $session->getline(Timeout=>$timeout); # Don't idle out. Of course, if you @power the Webster char # idle and hide, you won't have to worry about this. if ($counter++ > 1800) { $session->print("think I'm not idle!"); $counter = 0; } # If I see GOODBYE <password>, quit. $shutdown = 1, last if /^GOODBYE $safe_web_pass$/o; if (/^WEBSTERCONF: (\w+)/) { $server_type = $1; } # Otherwise, if I'm paged something, parse it if (/^(.*) pages: (.*)/) { my($player,$command,$name,@text) = ($1, split(" ", $2)); if ($name eq "Misspelled") { $name = "Checking user input for misspelled words"; } else { $name = "Checking $name for misspelled words"; } { # If the first word of the page is "spell", spellcheck the #rest. spell($player, $name, \@text), last if $command eq "spell"; # If it's define, look it up. lookup($player, \@text), last if $command eq "define"; error($player); } } } # Time to log out $session->print("QUIT"); # Get any exit messages until ($session->eof) { $_ = $session->get(Timeout=>$timeout); warn "$_\n" if $DEBUG; } warn "\nWebster completed\n"; $session->close; # If this is a full shutdown, exit the program if ($shutdown) { $LOG->close if $DOLOG; kill 9, $pid; # Just to be on the safe side, we'll make sure $SPELL->close; # the ispell process quits. $TOSPELL->close; exit 0; } # Otherwise, wait for a while and then come back later. sleep 300; } ########################################################################### # # Subrouting lookup - interface to DICT server. # sub lookup { my ($player, $words) = @_; my (@defs, $string, $DICT, $t, %dbs, @t); my $found = 0; $DICT = new Net::Dict($dict_server); if (!defined $DICT) { # Couldn't connect to the DICT server. Hrm $session->print("\@pemit/silent *$player=Dictionary unavailable"); return; } %dbs = $DICT->dbs(); @defs = map { $DICT->define($_) } parse_line('\s+', 0, "@$words"); foreach my $word (@defs) { foreach my $def (@$word) { my ($db, $d) = @$def; chomp $d; $d =~ s/([\[\]\%\(\)\\,])/\\$1/og; if ($server_type eq "PennMUSH") { $d =~ s/\{([^\}]*)\}/\\{[tagwrap(a, xch_cmd="+define \\"$1\\"", ansi(h, \{$1\}))]\\}/og; } else { $d =~ s/\{([^\}]*)\}/\\{[ansi(h, \{$1\})]\\}/og; } $d =~ s/\n/\%r/og; $db =~ s/([\[\]\{\}\%\(\)\\])/\\$1/og; $db =~ s/\n/\%r/og; $session->print("\@pemit/silent *$player=Definition from $dbs{$db}\%r$d"); $found = 1; } } if (!$found) { $session->print("\@pemit/silent *$player=No definitions found."); } $DICT->quit; } ########################################################################### # # Subroutine spell - interface to Unix ispell(1) # sub spell { my ($plr, $atr, $text) = @_; my (@answer, $ret); foreach (@$text) { $TOSPELL->print("^$_\n"); while (<$SPELL>) { # And read the output. chomp; last if $_ eq undef; # Exit the loop on the blank line # ispell prints after the last word so we know when to quit. warn "Reading $_ from ispell.\n" if $DEBUG; $ret = spell_parse_line $_; push @answer, $ret if $ret ne undef; } } if (@answer) { $session->print("\@pemit/silent *$plr=$atr:\%r@answer"); map {s/\%r/\n/} @answer if $DOLOG; # Pretty up the log file. $LOG->print(@answer) if $DOLOG; } else { $session->print("\@pemit/silent *$plr=$atr: None found."); } } sub spell_parse_line { my $first = substr $_[0], 0, 1; # Get the first letter of the string. if ($first eq '&' or $first eq '?') { # It was a bad word. my @line = split " ", $_[0]; return " Word: $line[1]. Suggestions: @line[4..$#line].\%r"; } if ($first eq '#') { # A bad word, with no suggestions. my @line = split " ", $_[0]; return " Word: $line[1]. No Suggestions.\%r"; } return; # Other lines don't require any return value. } sub error { my $player = shift; $session->print("\@pemit/silent *$player=Sorry, I don't know how to do that."); } sub REAPER { $SIG{CHLD} = \&REAPER; wait; } sub RELOAD { $SPELL->close; $TOSPELL->close; kill 9, $pid; $LOG->print("Reloading $local_dict at ", scalar localtime, "\n") if $DOLOG; $pid = open2 $SPELL, $TOSPELL, $ispell_prog, '-a', "-p$local_dict"; $SIG{USR1} = \&RELOAD; }