#!/usr/bin/perl # Program name: Statbot.pl # Author: Mason R. Hall (mrhall@fsu.edu) # The program interfaces with AOL instant messager. It logs into an IM account and # then periodically queries the person who is manning Virtual Reference for statistics. # It collects those stats and emails them out at the end of the day. # This programs runs once a day, using a crontab entry - in our case, set for 12:30am # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA use warnings; use strict; #Net::OSCAR is a perl module which creates an interface with AOL instant messenger service. #This module can be installed at the command line with the following commands: # >perl -MCPAN -e shell # > install Net::OSCAR # then answer "Y" to any dependancies - current version as of this release is 1.925 # See http://search.cpan.org/dist/Net-OSCAR/lib/Net/OSCAR.pm for additional information use Net::OSCAR qw(:standard); #Mail::Sender allows statbot to send email with attachments to the stat collectors. #This module can be installed at the command line with the following commands: # >perl -MCPAN -e shell # > install Net::OSCAR # then answer "Y" to any dependancies - current version as of this release is 0.8.13 # See http://search.cpan.org/~jenda/Mail-Sender-0.8.13/Sender.pm for additional information use Mail::Sender; use DBI; #Before run the program, create an AOL IM screen name for your bot at: http://dev.aol.com/aim/bots # Store the screen name and password in $sceenname and $password my $screenname = 'statbotScreenname'; my $password = 'statbotPassword'; #While we use meebo to connect to all the chat services, we use AOL for the statbot out of simplicity's sake. my $vrscreenname = 'VirtualRefernceAOLScreenname'; #Put your virtual reference's AOL screen name in the quotes here - #change this number to represent the minute on the hour you'd like statbot to ask for stats #In this case, the Statbot would ask for stats at 1:52, 2:52, 3:52, etc. #Since we operate with 1 hour shifts, we have the query towards the end of the hour my $timeToQuery = '52'; #This is the hour (in 24 hr time) when you'd like the statbot to send the stats for the day and shut down - its currently set for 9PM my $finishHour = '22'; #Set receipent's email address to receive stats and notices generated by this program my $statcollector = 'somebodys@email.edu'; #Set the smtp mailer address my $statcollectorsmtp = 'mail.smtp.edu'; #Set the directory you would like the statlogs stored my $statlogloc = '/var/www/statbot'; #Use these variables to schedule statbot. If the statbot shouldn't come online that day, use '99' for both the on and off times like shown for Saturday my $MonOn = '12' ; my $MonOff = '20' ; my $TueOn = '12' ; my $TueOff = '20' ; my $WedOn = '12' ; my $WedOff = '20' ; my $ThuOn = '12' ; my $ThuOff = '20' ; my $FriOn = '8' ; my $FriOff = '24' ; my $SatOn = '99' ; my $SatOff = '99' ; my $SunOn = '12' ; my $SunOff = '17' ; #This group of variables are needed for the Net::OSCAR module #Boolean value for away status: 1 = message recipient is away from computer my $is_away; #Boolean value for a fatal error message: 1 = fatal error, NetOSCAR could not connect my $fatal; #Error storing variable - catches errors thrown by the AOL system my $error; #The variable for the actual message text sent to the statbot my $message; my $statmessage; #Counter variable - keeps track steps during the user query process my $timekeeper = 0; #Boolean value for VR users answer status - Have they responded with the stats for the hour? 1 = Yes, they have responded this hour my $answer = 0; #Boolean variable for storing statbot's active status: 0 = it is not time for statbot to login yet, 1 = statbot is now logged in and ready to query my $logintime = 0; #Stores am/pm value for conversion between 24 hour time and standard. my $ampm = 'am'; #Variable for storing lines of the flatfile when processing my $line; # my $staterror; my $sender; #These are the actual queries sent to the person who is on Virtual Reference. They are produced one after another, each a minute apart - hence the more incessant nature of each one. We found "prodding" the user with multiple replies #was usually more effective than the same one over and over again. Change these as you see fit. my $statQuery1 = 'Hi! I\'m statbot! How many reference questions have you answered in the past hour? Please reply with numbers only.'; my $statQuery2 = 'Hi again! I\'m statbot! How many reference questions have you answered in the past hour? Please reply with numbers only.'; my $statQuery3 = 'Hello, I\'m still waiting for your answer. Please tell me how many reference questions have you answered in the past hour? Please reply with numbers only.'; my $statQuery4 = 'Please enter the number of reference questions have you answered in the past hour. Please reply with numbers only.'; #This is the final message sent to the user when they do not respond AT ALL to the statbot's queries. An email is also sent to the supervisor when this message is sent. my $statQueryFailed = '**** Your VR session has been logged as not recording stats. A supervisor will contact you shortly. ****'; #This while loop checks the schedule to see if statbot should come online for the day. while ($logintime == 0) { my($second, $minute, $hour, $dayOfMonth, $month, $yearOffset, $dayOfWeek, $dayOfYear, $daylightSavings) = localtime(); #This is the scheduling section which checks against the user provided on/off times above #SCHEDSWITCH is a case statment - meaning it will check the each statment until it finds one that is true SCHEDSWITCH: { if (($dayOfWeek == 1 ) && ($hour >= $MonOn) && ($hour <= $MonOff)) {$logintime = 1; last SCHEDSWITCH;} if (($dayOfWeek == 2 ) && ($hour >= $TueOn) && ($hour <= $TueOff)) {$logintime = 1; last SCHEDSWITCH;} if (($dayOfWeek == 3 ) && ($hour >= $WedOn) && ($hour <= $WedOff)) {$logintime = 1; last SCHEDSWITCH;} if (($dayOfWeek == 4 ) && ($hour >= $ThuOn) && ($hour <= $ThuOff)) {$logintime = 1; last SCHEDSWITCH;} if (($dayOfWeek == 5 ) && ($hour >= $FriOn) && ($hour <= $FriOff)) {$logintime = 1; last SCHEDSWITCH;} if (($dayOfWeek == 6 ) && ($hour >= $SatOn) && ($hour <= $SatOff)) {$logintime = 1; last SCHEDSWITCH;} if (($dayOfWeek == 0 ) && ($hour >= $SunOn) && ($hour <= $SunOff)) {$logintime = 1; last SCHEDSWITCH;} #If statbot shouldn't be online yet, go to sleep for 10 minutes (600 seconds) and try again afterwards else {sleep 600; last SCHEDSWITCH;} }#End of SHEDSWITCH case statment }#End of while ($logintime == 0) loop #Change nothing below this line -------------------------------------------------------------------------------------------------------------------------------------------------------- #This section uses the Net::OSCAR module to login to AOL instant messenger and set up some procedures my $oscar = Net::OSCAR->new(); #This line sets the sub (im_in) when a message is sent to statbot - therefore anytime a message is sent directly to statbot, sub im_in will be run $oscar->set_callback_im_in(\&im_in); #This line sets the sub (got_error) when an error is thrown - therefore anytime an error is thrown, sub got_error will be run $oscar->set_callback_error(\&got_error); #This line actually logs the statbot into AOL instant messenger with the provided username and password $oscar->signon($screenname, $password); #Once the statbot is logged in, the script will loop continuously, waiting to be activated by either the given query time ($timeToQuery) or an outside action like an IM or error while(1) { #This command tells the Net:OSCAR module to do an entire loop of its processes - this includes checking for incoming messages, sending messages, and checking statuses $oscar->do_one_loop(); my($second, $minute, $hour, $dayOfMonth, $month, $yearOffset, $dayOfWeek, $dayOfYear, $daylightSavings) = localtime(); #This case statement will query the employee on Virtual Reference starting at the $timeToQuery and ask every minute for 5 minutes until giving up and reporting a "no report" back to the stat collector TIMESWITCH: { if ($minute == ($timeToQuery - 1)) {$timekeeper = 1;last TIMESWITCH;} #Minute 1 if ($minute == $timeToQuery && ($timekeeper== 1)) {$oscar->send_im($vrscreenname,$statQuery1); $timekeeper= 2; last TIMESWITCH;} #Minute 2 if ($minute == ($timeToQuery + 1) && ($timekeeper == 2)) {$oscar->send_im($vrscreenname,$statQuery2); $timekeeper= 3;last TIMESWITCH;} # Etc... if ($minute == ($timeToQuery + 2) && ($timekeeper == 3)) {$oscar->send_im($vrscreenname,$statQuery3); $timekeeper= 4;last TIMESWITCH;} if ($minute == ($timeToQuery + 3) && ($timekeeper == 4)) {$oscar->send_im($vrscreenname,$statQuery4); $timekeeper= 5;last TIMESWITCH;} # Minute 5, no message sent here - script begins noting that the user did not respond to its messages if ($minute == ($timeToQuery + 4) && ($timekeeper == 5)) { $oscar->send_im($vrscreenname,$statQueryFailed); #Simple conversion for 24 hr to 12 hr time if ($hour >= 12) {$hour = $hour - 12;$ampm = 'pm';} if ($hour == 0) {$hour = 12;} #Now that no one has answered the query after the 5th attempt, the script opens and notates the statlog.txt as "NR" or "No Response" open (STATSLOG, ">>$statlogloc/statlog.txt"); print STATSLOG "$hour$ampm\t0\tNR\n"; close (STATSLOG); #After recording the "No Response" the script also sends the stat collector an email, notifying them that they will not have stats for this hour $sender = new Mail::Sender {from => $statcollector ,smtp => $statcollectorsmtp}; if (ref ($sender->MailMsg( { to =>$statcollector, subject => "VR Stats not recorded for $hour $ampm", msg => "This is an automatic notification - please do not reply to this message.\n\nStatistics were not recorded for virtual reference for the $hour $ampm hour. Please contact the appropriate individual and record statistics manually. " }))) {print "Mail sent OK."} else {die "$Mail::Sender::Error\n";} #The 5 minute counter is reset for the next hour here. $timekeeper= 0; last TIMESWITCH; } #Don't think this line is needed, but just in case... if ($minute == ($timeToQuery + 5)) {$timekeeper= 0; last TIMESWITCH;} #This if then checks to see if it is time ($finishHour) to shut down and report all statistics for the day. if (($hour == $finishHour && $minute == 01 && $second == 01)){ #Once it's time to end the day, statbot opens the statlog flatfile and begins parsing the lines one at a time open (STATSLOG, "$statlogloc/statlog.txt"); my ($stathour, $statnumber, $staterror, $total, $tally, $divtally) = 0; while ($line = ) { #With each new line, the script tallies the total number of stat questions that day ($stathour, $statnumber, $staterror) = split (/\t/, $line); $total = $total + $statnumber; $tally++; } close (STATSLOG); #The statbot figures the average number of questions per hour. Any other relevant stats could be generated in this section as well. #The next line prevents div by zero issues if ($tally == 0) {$divtally = 1;} else {$divtally = $tally;} $average = sprintf("%.3f", ($total/$divtally)); open (STATSLOG, ">>$statlogloc/statlog.txt"); #Total and average number of VR questions are stamped at the end of the file print STATSLOG "Total\t$total\nAverage\t$average"; close (STATSLOG); #The entire statlog.txt file is sent off to the statcollector for viewing/storage/manipulation... $sender = new Mail::Sender {from => $statcollector,smtp => $statcollectorsmtp}; $sender->OpenMultipart({to=> $statcollector,subject=> "Daily VR Stats - $month-$dayOfMonth"}); $sender->Body(); $sender->Attach({ encoding => 'Base64', description => 'statlog.txt', ctype => 'text/plain', disposition => "attachment; filename = 'statlog.txt'", file => "$statlogloc/statlog.txt" }); $sender->Close(); print "Content-type: text/plain\n\nYes, the stats are sent\n\n"; #The statlog.txt is renamed with the date appended to the end, and a new clean statlog.txt is created system("mv $statlogloc/statlog.txt $statlogloc/statlog-$month-$dayOfMonth.txt"); system("touch $statlogloc/statlog.txt"); #The statbot is done for the day, goes to bed die; last TIMESWITCH; } } #As defined by the "$oscar->set_callback_im_in(\&im_in);" line above, this sub will activate ANYTIME a message is sent to statbot sub im_in { #Throws all neccessary variables into an array. The important ones here are "$sender" and "$message" my($oscar, $sender, $message, $is_away) = @_; print "[AWAY] " if $is_away; #Cleans message of all weird formating $message =~ s/<(.|\n)+?>//g; #If an administrator wishes to view the stats for the day, the can send the message "adminstats" to the statbot. It will return the current total to the user if ($message eq "adminstats") { open (STATSLOG, "$statlogloc/statlog.txt"); my ($stathour, $statnumber, $staterror, $total, $tally) = 0; while ($line = ) { ($stathour, $statnumber, $staterror) = 0; ($stathour, $statnumber, $staterror) = split (/\t/, $line); $total = $total + $statnumber; $tally++; } close (STATSLOG); $oscar->send_im($sender,"There have been $total reference questions today"); return; } #If an administrator is away, but statbot needs to be shut down for the day, sending the message "gotobednow" will shut the statbot down for the day elsif ($message eq "gotobednow") { #The statlog.txt is renamed with the date appended to the end, and a new clean statlog.txt is created system("mv $statlogloc/statlog.txt $statlogloc/statlog-$month-$dayOfMonth.txt"); system("touch $statlogloc/statlog.txt"); die; } #This section becomes active once the script has asked for the hourly stats elsif ($answer == 0) { #if the statbot has not asked for stats yet, any other messages to statbot will result in this message if ($timekeeper== 0) { $oscar->send_im($sender,"I'm busy right now - please wait until I ask for your stats"); } else { #Statbot checks to make sure query reply is a number and nothing else unless ($message =~ /^\d+$/) { $oscar->send_im($sender,"*** That is not a number. Please use only whole numbers! *** How many reference questions have you answered in the past hour? Please reply with numbers only. "); return; } #Statbot prompts user to double check their statistics if they enter an unusually high number if ($message >= 20) { $oscar->send_im($sender,"*** That is a very high number. Please make sure this number is correct. ***"); } #Statbot asks user to confirm their message $oscar->send_im($vrscreenname,"You entered: $message. If this is correct please press Y for yes or N for no."); $statmessage = $message; $timekeeper= ($timekeeper+10); #Once this point is reached, any reply by a user will be taken by the next section - for confirmation and writing to the statlog.txt $answer = 1; return; } }#End of elsif ($answer == 0) statement #Now that the user has entered a number for their hourly stats, the script will confirm and record those stats elsif ($answer == 1) { my($second, $minute, $hour, $dayOfMonth, $month, $yearOffset, $dayOfWeek, $dayOfYear, $daylightSavings) = localtime(); #Simple conversion from 24 hour to 12 hour time if ($hour >= 12){$hour = $hour - 12;$ampm = 'pm';} if ($hour == 0) {$hour = 12;} #Statbot checks for a yes or no response from the user, to verify that the number of stats they entered in the elsif statement above is correct if ($message=~/[y]/i) { #If the number is correct, the script writes the time and number of stats to the statlog.txt $oscar->send_im($vrscreenname,"Thanks! Your stats have been recorded!"); open (STATSLOG, ">>$statlogloc/statlog.txt"); print STATSLOG "$hour$ampm\t$statmessage\t\n"; close (STATSLOG); #answer and time counters are reset here, now ready for the next hour of stat collection $timekeeper= 0; $answer = 0; return; } elsif ($message=~/[n]/i) { #If this number was wrong, the statbot resets the "answer" counter so the user can try again $oscar->send_im($vrscreenname,"We all make mistakes. Please enter the number of reference questions have you answered in the past hour. Please reply with numbers only."); $answer = 0; return; } #If the user's finger slips, statbot reminds them that it can only take a Y or N answer else {$oscar->send_im($vrscreenname,"I need a Y or N. If the stats entered are correct, please press Y for yes or N for no.");} #return from the im_in sub return; }#End of elsif ($answer == 1) statement }#End of im_in sub #this section records and handles any errors that may occur with AOL connections. See the file marked "error.log" sub got_error { my($oscar, $connection, $error, $errortype, $fatal) = @_; my($second, $minute, $hour, $dayOfMonth, $month, $yearOffset, $dayOfWeek, $dayOfYear, $daylightSavings) = localtime(); if ($fatal == 1 ) { open (ERRORLOG, '>>error.log'); print ERRORLOG "$month/$dayOfMonth $hour:$minute\n $error\n $errortype\n"; close (ERRORLOG); sleep 60; $oscar->signon($screenname, $password); } else { open (ERRORLOG, '>>error.log'); print ERRORLOG "$month/$dayOfMonth - $hour:$minute\n $error : $errortype\n"; close (ERRORLOG); } #this is the error for when a user is not logged in - it "tattles" to the statkeeper if ($error == 4) { if ($hour >= 12) {$hour = $hour - 12;$ampm = 'pm';} if ($hour == 0) {$hour = 12;} open (STATSLOG, ">>$statlogloc/statlog.txt"); print STATSLOG "$hour$ampm\t0\tNL\n"; close (STATSLOG); $sender = new Mail::Sender {from => $statcollector,smtp => $statcollectorsmtp}; if (ref ($sender->MailMsg({ to =>$statcollector, subject => "VR not logged in for $hour $ampm hour", msg => "This is an automatic notification - please do not reply to this message.\n\nThe VR user was not logged in for the $hour $ampm hour. Please contact the appropriate individual. " }))) {print "Mail sent OK."} else {die "$Mail::Sender::Error\n";} $timekeeper= 0; }#End of if ($error == 4) statement return; }#End of got_error sub }#End of while(1) loop