DoctorGibbs' Perl Toolkit for Interacting with the There World

Part 6. Teleporting Your Avatar (and also interacting with main menu and webapps.prod.there.com)

In this section we will learn several ways to teleport your avatar. Of these, the most powerful involve interacting with There's server at http://webapps.prod.there.com (hereafter referred to as WPTC). Once we learn how to do this interaction, we will also be able to use other services from WPTC.

We will also use this section to show how to interact with the Main Menu in your There window -- also something that will be useful not just for teleporting.

So, this is an important section even if you are not interested in teleporting.

First let's consider the case of teleporting to a destination that you have bookmarked previously as a favorite place. That means that we want to simulate, one way or another, a click on the Main Menu choice Places/Go To Favorite/[YourSavedPlacename] . The most direct, and least elegant, way to do this is to send mouse clicks to the correct pre-measured geometrical place in the There window, for example as follows

sub TeleportToFavorite {
    my ($therewin,$favno) = @_;
    Win32::GuiTest::SetForegroundWindow($therewin);
    sleep 1;
    ($x,$y) = Win32::GuiTest::GetWindowRect($therewin);
    Win32::GuiTest::MouseMoveAbsPix($x+242,$y+613);
    sleep 1;
    Win32::GuiTest::SendMouse("{LEFTCLICK}"); # Places button
    sleep 1;
    Win32::GuiTest::MouseMoveAbsPix($x+242,$y+542);
    sleep 1;
    Win32::GuiTest::SendMouse("{LEFTCLICK}"); # Go to Favorites button
    sleep 1;
    Win32::GuiTest::MouseMoveAbsPix($x+356,$y+542);
    sleep 1;
    Win32::GuiTest::MouseMoveAbsPix($x+356,$y+612-23*$favno);
    sleep 1;
    Win32::GuiTest::SendMouse("{LEFTCLICK}"); # Teleport!
    sleep 20;
    Win32::GuiTest::MouseMoveAbsPix($x+100,$y+100);
    sleep 1;
    Win32::GuiTest::SendMouse("{LEFTCLICK}"); # click on main window
}
The arguments are the There window ID as returned from GetThereWindow(), and an integer indicating which favorite place, counting from the bottom of the displayed list up, you want to click on. This works reliably, but only if (1) your favorite places all have short enough names to fit on one line in the pop-up menu, and (2) the desired bookmark is visible in the pop-up (i.e., you don't have to scroll to reach it). Actually, I've included TeleportToFavorite only as an illustration of how to use mouse clicks to interact with the There gui. There are better ways to teleport!

The better way to get to a bookmarked favorite place is to use the interface to the Main Menu provided by localhost:9999. The global URL for this (defined in TherePerlGlobals.pl) is
$URLmainMenu = "http://localhost:9999/ScriptHook/Invoke?Path=%2FmainMenu%2F";
with some stuff appended to define which menu choice you want. A subroutine for this is

sub MainMenuChoice {
    my @clicks = @_;
    my $click;
    my $url = $URLmainMenu;
# must take window focus, else MainMenu is unavailable
    Win32::GuiTest::SetForegroundWindow(&GetThereWindow());
    foreach $click (@clicks) {
	$click =~ s/ /+/g;
	$click =~ s/\'/%27/g;
	$url .= ($click . "%2F");
    }
    $url .= "activate";
    $req = HTTP::Request->new('GET', $url);
    $res = $ua->request($req);
    my $ret = $res->content;
    if ($ret =~ m/([^<]+)<\/ErrorCode>/) { return $1; }
    else {return 0;}
}
A sample call to MainMenuChoice() is
$errcode = &MainMenuChoice(("My Things", "Vehicles", "Bumble Bee", "Take Out"))
If $errcode returns as zero all is well. Otherwise all is not well. I have no idea how to interpret the error codes.

A few things are worth noting: (1) The double parentheses are of course because the subroutine is looking for an array as its single argument. (2) The array elements must be exactly a series of available menu choices. Your bumble bee might not be in a folder named "Vehicles" the way mine is, for example. Capitalization and spaces are significant. (3) I'm assuming that space and apostrophe are the only special characters in your menu. If otherwise, steal from somewhere the line or two of code to convert special characters to %xx notation -- I always forget how to do this.

A example of teleporting to a favorite, then, is

$errcode = &MainMenuChoice(("Places", "Go To Favorite", "Duda Beach"));
where, again, the text strings must correspond exactly to the choices that your There menu displays.

Now let's get to the fun part, namely interacting with WPTC!

Conceptually, all you need to do to teleport to a doid that is an allowed teleport destination (houses, PAZs, signs, etc.) is to hit on the URL
$URLteleDoid = "http://webapps.prod.there.com/goto/goto?obj=";
(appending a doid number) or on the URL
$URLtelePlace = "http://webapps.prod.there.com/goto/goto?placename="; (appending a placename that allows teleports). Sounds easy.

What makes it not easy is that your perl script is running in a window that was NOT spawned by the There client. The WPTC server knows this, and therefore requires AvatarName/Password authentication before it is willing to honor the request. (Actually it is very nice of the There-gods to allow such "foreign" requests at all. They could have required that WPTC requests come from There browsers without exception. Thank you, There-gods. We promise not to abuse the privilege!)

Don't even try to read any further unless you have installed the HTTPS patch into LWP, as described in Part 1, and verified that you can use LWP to retrieve https:// web pages!

So here is what is supposed to happen when you hit on a URL like $URLteleDoid above:

  • You hit on the URL by the GET method.
  • WPTC returns a login page containing a login form, and also a hidden input field, named "redirect", with the URL you originally sent.
  • You fill in the avatar name and password and submit the form by the HTTPS protocol and the POST method.
  • WPTC validates the login information, reads the redirect request, and sends back a client-side redirect to the original URL. It may (I'm not sure) also send back a cookie.
  • Your browser follows the redirect and hits the original URL a second time.
  • This time, WPTC knows that you are validated (by maintaining state? or by the cookie?), so it performs the desired action (e.g., teleport) and returns an XML confirmation. If you do this, by hand, in a browser, you can see all the steps happening before your eyes, especially on a congested internet connection (where the steps go slowly). It should be straightforward to simulate this whole protocol with LWP in Perl. Should be, but isn't; for some reason I've never been able to get it to work correctly. When I send back the POST'd form, I get back the expected redirect all right. But when I follow that redirect, I get a second request to login, and so on forever.

    If anyone can solve this puzzle, please post Perl code on the There forum! I'm sure I must be doing something stupid.

    Luckily, there is a good workaround: If I POST back the login form omitting the hidden redirect input field, WPTC is nice enough to set a cookie indicating that I am now logged in for the duration of the perl script's execution. After that, I can just GET the original URL -- and any other WPTC URLs -- without difficulty. I think this is a feature, not a bug, and therefore it should be robust. But if I am wrong, and it is actually a bug, then it might get fixed someday! So it would be nice to be able to simulate exactly the actual protocol.

    At any rate, here is a subroutine for getting an arbitrary WPTC URL, logging in if required (which will generally be only once per execution of the Perl script):

    sub GetWebappsURL {
        my ($wptcURL,$avatar,$passwd) = @_;
        if ($wptcURL !~ m/^${URLwptc}/i) {
    	return "First arg must begin with $URLwptc\n";
        }
        $req = HTTP::Request->new('GET' => $wptcURL);
        $res = $ua->request($req);
        my $response = $res->content;
        if ($response =~ m/]+URL=([^\"]+login\?)redirect=[^\"]+\"/) {
    	$head = $1;
    	if ($debug) {print "LOGGING IN.....\n";}
    	my $loginARG = "sendAsLoginData=1&avname=$avatar&password=$passwd";
    	$req = HTTP::Request->new('GET' => $head.$loginARG);
    	$res = $ua->request($req);
    	$response = $res->content;
    	if ($response =~ m/Login Succeeded!/) {
    	    $req = HTTP::Request->new('GET' => $wptcURL );
    	    $res = $ua->request($req);
    	    $response = $res->content;
    	    return $response;
    	} else {
    	    return "Login Failed!\n";
    	}
        }
        return $response;
    }
    
    The three arguments are the (full) desired URL, your avatar name, and your password. The global string $URLwptc is
    $URLwptc = "http://webapps.prod.there.com";.

    You might wonder where is the GET from an HTTPS URL that required the HTTPS patch? The answer is that $head, when set from the WPTC return string that contains "redirect=", always begins with "https://".

    To make cookies work (necessary for the above) you need these declarations at the beginning of your script,

    use HTTP::Cookies;
    $cookie_jar = HTTP::Cookies->new(file => "lwp_cookies.dat", autosave => 1);
    $ua->cookie_jar($cookie_jar);
    
    I think it's also a good idea to declare
    $ua->agent("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)");
    
    so that your script looks like an MSIE browser to WPTC.

    (Another exercise for the reader: Is it possible to make a successful login "stick" for more than the execution of a single script? By saving and rewriting the cookie at appropriate times, for example?)

    We now have all the machinery needed to teleport to objects or placenames (those that allow teleporting-to, of course):

    sub TeleportToPlace {
        my $place = shift;
        $place =~ s/ /+/g;
        $place =~ s/\'/%27/g;
        my $resp = &GetWebappsURL($URLtelePlace . $place, $myavname, $mypassword);
        if ($resp =~ m/<Value>([^<]+)<\/Value>/) {
    	return $1;
        } else {
    	return 99;  # unable to parse return value
        }
    }
    
    sub TeleportToDoid {
        my $doid = shift;
        my $resp = &GetWebappsURL($URLteleDoid . $doid, $myavname, $mypassword);
        if ($resp =~ m/<Value>([^<]+)<\/Value>/) {
    	return $1;
        } else {
    	return 99;  # unable to parse return value
        }
    }
    
    These are called with a single argument, the doid number or place name. They get the login avatar name and password from the global variables $myavname and $mypassword, which you must have set. They return zero on success, and an error number on a recognizable failure.

    Happy teleporting!

    Next: Part 7