# Googlematic::Responder
# (c) 2002 Matt Webb <matt@interconnected.org> All rights reserved
#
# Each buddy talking to Googlematic has their own Responder that lasts as long
# as the user-session does. Each Responder is a session of its own under POE,
# and persists for a couple of minutes. It ties together the following
# functionality:
# - Google searches, provided by the 'google' POE session (Googlematic::Search)
# - IM in and out, provided by the 'im' POE session (Googlematic::IM)
# - Navigating through the results set
#
# There are a number of paths through the Responder.
#
# INCOMING IM MESSAGES
# - All incoming Instant Messages go through the in state (even the first message,
#   which passes through the _start state first).
# - The in state maintains the conversational position and decides which state is
#   going to deal with the user's request. It might be a new search request, a
#   request to look at the list of results, or a request to see results details
#
# GOOGLE SEARCH RESULTS
# - The Google search session ('google') posts into one of three states
#   depending on what the result of the search was. The states are:
#   - remember, for a successful search
#   - limit, if too many searches are being made at the moment
#   - error, for some unknown error
#   All of these three states result in a message being returned to the user.
#
#
# The Conversation User Interface has states too, which are kept in the
# variable $heap->{waiting_for}. Each state is anticipating different things
# from the user. The three possible CUI positions are:
# - 'query': The next thing the user sends will be a search query. This is the
#   default position, if the Responder has just started or the previous search
#   wasn't successful.
# - 'more': The next thing the user sends will either be the word "more"
#   or a new search query. This is when just the first search result has been
#   returned.
# - 'detail': The nexy thing the user sends will either be the word "more",
#   a number (to see more details about that search result) or a search query.
#   This is when a list of results has been given.
#
# Additional we track what search results have been sent to the user so that
# typing the word "more" gives a sensible result.


package Googlematic::Responder;

use strict;
use POE::Session;
use CGI qw/escape unescape/;
use HTML::Entities;


# _start
# - Sends the default welcome message to the user, and plugs Google.
# - Passes the message off to the in state
# - Registers itself with the 'im' session by returning the interface and
#   buddy name. This is caught by the _child state of the 'im' session.
sub _start {
    my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

    # Postpone the default death of this Responder by two minutes
    $kernel->delay( time_is_up => $Googlematic::CONFIG->{user_session_timeout} );

    # Store incoming information
    $heap->{interface} = $_[ARG0];
    $heap->{buddy_name} = $_[ARG1];
    my $message = $_[ARG2];

    # Log this new Responder
    print "New responder starting for <" . $heap->{buddy_name} . "> with message <$message>\n";

    # There are three CUI waiting states:
    # query, more, detail [detail can also mean 'more again']
    $heap->{waiting_for} = "query";

    # Send default greeting
    my $greeting;
    if($heap->{interface} eq 'aim') {
	$greeting = "Hi there. I'm searching <a href=\"http://www.google.com/\">Google</a> for you...";
    }
    $kernel->post("im", "send", $heap->{interface}, $heap->{buddy_name}, $greeting);
 
    # The in state deals with all incoming IM messages
    $kernel->yield('in' => $message);

    return [ $heap->{interface}, $heap->{buddy_name} ];
}


# in($message)
# - Tracks the CUI state
# - Depending on the CUI state and what the message is, either:
#   - Does a Google search 
#   - Returns details of a search result to the user
#   - Returns a portion of the results summary to the user
#   - Returns help information
#   The first three of these correspond to other POE states within this session
sub in {
    my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
    
    # Delay the death of this Responder by a couple of minutes
    $kernel->delay( time_is_up => $Googlematic::CONFIG->{user_session_timeout} );

    my $message = $_[ARG0];

    # A bit of special behaviour. If the message includes the word googlematic
    # then return a link to the webpage and a short message.
    if($message =~ m/\bgooglematic\b/i) {
	my $message;
	if($heap->{interface} eq 'aim') {
	    $message = "<a href=\"http://interconnected.org/googlematic/\">Googlematic</a> is searching... (click for more)";
	}
	$kernel->post("im", "send", $heap->{interface}, $heap->{buddy_name}, $message);
    }

    # Decide what to do with the incoming Instant Message
    if($heap->{waiting_for} eq 'more' && $message =~ /^more$/i) {
	# The CUI position 'more' means the user has only seen the top
	# search result. The list state will show them the top 5 results
	$kernel->yield('list');
    }
    elsif($heap->{waiting_for} eq 'detail' && $message =~ /^more$/i) {
	# The CUI position 'detail' means the user has already seen the
	# top search results. The list state can show them the next
	# 5 results.
	$kernel->yield('list', 5);
    }
    elsif($heap->{waiting_for} eq 'detail'
	  && $message =~ /^\d+$/
	  && defined($heap->{last_results}->[$message - 1])) {
    	# The CUI position 'detail' means the user has seen some summary
    	# search results, which are listed by number. If they type a
    	# number which was just on screen, show them details about that result.
    	# The detail state will take care of that.
	my $num = $message - 1;
	$kernel->yield('detail', $num);
    }
    elsif($message =~ m/^\s*$/) {
    	# The user hasn't typed anything. Send some helpful information.
	$kernel->post("im", "send", $heap->{interface}, $heap->{buddy_name}, "Tell me something, and I'll search Google.");
    }
    else {
    	# A special command hasn't been recognised. It must be a search
    	# requests. Don't send an IM to the user -- the Google search will
    	# return to one of three states in this session, and that state
    	# will take care of talking to the user.
	$heap->{last_query} = $message;
	$kernel->post("google", "search", $heap->{interface}, $heap->{buddy_name}, $message);
    }
}


# remember
# - 1st of 3 states posted back to by a Google search request
# - Called on a successful search
# - Remember the results set
# - Give the user details about the first one
sub remember {
    my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
    
    # Store these search results for later	
    $heap->{last_results} = $_[ARG0];
    
    # Go through all the results, and remove the HTML encoding
    foreach my $result ( @{$heap->{last_results}} ) {
	my @hit = qw/title snippet summary/;
	foreach my $hit (@hit) {
	    next unless $result->{$hit} !~ m/^\s*$/;
	    # Get rid of <b>, </b>, and <br>, however they're encoded
	    $result->{$hit} =~ s[&lt;/?br?&gt;][]g;
	    $result->{$hit} =~ s[</?br?>][]g;
	    $result->{$hit} = decode_entities($result->{$hit});
	    # ...sometimes double encoded:
	    $result->{$hit} = decode_entities($result->{$hit});
	}
    }

    # Move into the next CUI position	
    $heap->{waiting_for} = "more";
    
    # The detail state tells the user about the search result in the index
    # passed through. In this case, the first one	
    $kernel->yield('detail', "0");
}


# limit
# - 2nd of 3 states posted back to by a Google search request
# - Unsuccessful search because the 'google' session is doing too many
#   searches
# - Tells the user that
sub limit {
    my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

    my $message = "Sorry, I'm doing too many searches at the moment. Please ask again in a couple of hours.";
    $kernel->post("im", "send", $heap->{interface}, $heap->{buddy_name}, $message);
}


# error
# - 3rd of 3 states posted back to by a Google search request
# - Unsuccessful search for some unknown reason
# - Tells the user that
sub error {
    my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

    # google search error
    my $message = "Sorry, an unexpected error occurred. (Unexpected.. aren't they always?) Please try later."; 
    $kernel->post("im", "send", $heap->{interface}, $heap->{buddy_name}, $message);
}


# detail($result_index)
# - Tells a user details about the search result in the index passed through
# - The logic is to construct a sensible looking details message
# - For other interfaces, the message construction might be different
sub detail {
    my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

    # The results from the most recent search.	
    my $result = $heap->{last_results}->[ $_[ARG0] ];
    
    my $reply = "Searched for \"" . $heap->{last_query} . "\" and found ";	

    my $url = "";
    if(exists $result->{URL} && ($result->{URL} !~ m/^\s*$/)) {
	$url = $result->{URL};
    }
    else {
	# If there's no URL for this search result, send back an error
	my $message = "Didn't find any results for \"" . $heap->{last_query} . "\"";
	if($heap->{interface} eq 'aim') {
	    $message = "<i>$message</i>";
	}
	$kernel->post("im", "send", $heap->{interface}, $heap->{buddy_name}, $message);
	return;
    }
	
    if($heap->{interface} eq 'aim') {
	if(exists $result->{title} && ($result->{title} !~ m/^\s*$/)) {
	    $reply .= "<a href=\"$url\">" . $result->{title} . "</a>";
	}
	else {
	    $reply .= "<a href=\"$url\">$url</a>";
	}
	if(exists $result->{summary} && ($result->{summary} !~ m/^\s*$/)) {
	    $reply .= ": <i>" . $result->{summary} . "</i>";
	}
	elsif (exists $result->{snippet} && ($result->{snippet} !~ m/^\s*$/)) {
	    $reply .= ": <i>" . $result->{snippet} . "</i>";
	}
    }
    
    $kernel->post("im", "send", $heap->{interface}, $heap->{buddy_name}, $reply);

}


# list( [$start] )
# - Returns a summary of 5 results, numbered, to the user. This is the top 5
#   if unspecified, or a different 5 if the parameter is passed in.
# - Remembers the numbers so a detailed result can be requested by the user
# - The list construction can be different for different interfaces
sub list {
    my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
    
    my $start = $_[ARG0] || 0;
    
    # Holds the lines of this message.
    my @reply_fragments = ("Top sites:");
    
    for(my $i = $start; $i <= $#{ @{ $heap->{last_results} } } && $i < $start+5; ++$i) {
	my $result = $heap->{last_results}->[$i];
	my $num = $i + 1;
	if($heap->{interface} eq 'aim') {
	    if($result->{title} !~ m/^\s*$/) {
		push @reply_fragments, "<b>$num</b> - <a href=\"" . $result->{URL} . "\">" . $result->{title} . "</a>";
	    }
	    else {
		push @reply_fragments, "<b>$num</b> - <a href=\"" . $result->{URL} . "\">" . $result->{URL} . "</a>";
	    }
	}
    }

    # If this is the bottom 5 of the search results, include a link so the user
    # can continue their search at the Google website.
    if($start == 5) {
	my $google_url = "http://www.google.com/search?hl=en&q=" . escape($heap->{last_query});
	if($heap->{interface} eq 'aim') {
	    push @reply_fragments, "<i>...or <a href=\"$google_url\">continue this search at Google.com</a>.</i>";
	}
    }
    
    my $linebreak = "<br>";
    my $reply = join($linebreak, @reply_fragments);
    
    # Put this Responder into the 'detail' CUI position, which expects either
    # a number, the word "more" or a new search query next
    $heap->{waiting_for} = "detail";
	
    $kernel->post("im", "send", $heap->{interface}, $heap->{buddy_name}, $reply);
}


return 1;
