Important Update
Since this article was written, AOL has shut down their TOC servers and replaced them with TOC2 servers which only accept the TOC2 protocol. An excellent PHP library for this protocol can be found at
http://sourceforge.net/projects/phptoclib/
This article will remain as a sort of legacy.
So you want to run a bot on AOL Instant Messenger? Want to play a joke on your friends, or have instant access to database results, or be able to get movie showtimes or the weather? Really, a bot is ideal to serve as a data retrieval servant for a number of web applications. Here is some strung about code to use AOL's TOC protocols to put a bot online and able to respond to user input.
Step 1: Terminology and Requirements
Basically, Instant Messenger works on a very simple premise. It accepts a flap of data from a user (FLAP standing for FDDITalk Link Access Protocol, which is used by OSCAR, which is AIM's own protocol), which contains a message and another user who serves a recipient. This message may not just be a text message - it can also be an invitation to join a chat room, a warning, an auto response, or to notify the other user that they have signed on or off AIM. All of this data is sent via the TOC protocol, which can be simulated. It is this somewhat open standard which resulted in GAIM and other AIM-accessible extensions and substitutes. For our purposes, our bot will run independent of any client besides a command prompt and a PHP server.
To run this code, you will need:
- A PHP server with command line interface (CLI) capability. By default, all installations of PHP are CLI-compatible.
- An active AOL Instant Messenger account that will be your bot. Amazingly, you do not actually need the AIM software on your computer to run this bot.
- Some sort of command prompt. I know this code works in Windows; I assume it works in Linux and other OSes as well, since the code is platform-independent, based only on PHP and TCP availability.
- Optionally, a connection to one of many database types allowed to interface with PHP. mySQL is the default for this code, but it can easily be altered to generate data based on ODBC (Microsoft Access), PostGreSQL, SQLite, dBase, or even generated XML files, as well as other, more esoteric options should the need arise.
Step 2: Defining Variables
The first thing with any sort of organized call-and-response system is to build in its own preset variables (and especially error messages) into your surrogate system. If AIM is inaccessible or a bad packet is sent, it's good to know what's going on within your bot's PHP code as easily as you would if you were using AIM.
define("MAX_PACKLENGTH",65535);
define("SFLAP_TYPE_SIGNON",1);
define("SFLAP_TYPE_DATA",2);
define("SFLAP_TYPE_ERROR",3);
define("SFLAP_TYPE_SIGNOFF",4);
define("SFLAP_TYPE_KEEPALIVE",5);
define("SFLAP_MAX_LENGTH",4096);
define("SFLAP_SUCCESS",0);
define("SFLAP_ERR_UNKNOWN",1);
define("SFLAP_ERR_ARGS",2);
define("SFLAP_ERR_LENGTH",3);
define("SFLAP_ERR_READ",4);
define("SFLAP_ERR_SEND",5);
define("SFLAP_FLAP_VERSION",1);
define("SFLAP_TLV_TAG",1);
define("SFLAP_HEADER_LEN",6);
ini_set("max_execution_time", "0");
$ERROR_MSGS = array ( 0 => 'Success',
1 => 'AOLIM Error: Unknown Error',
2 => 'AOLIM Error: Incorrect Arguments',
3 => 'AOLIM Error: Exceeded Max Packet Length (1024)',
4 => 'AOLIM Error: Reading from server',
5 => 'AOLIM Error: Sending to server',
6 => 'AOLIM Error: Login timeout',
901 => 'General Error: $ERR_ARG not currently available',
902 => 'General Error: Warning of $ERR_ARG not currently available',
903 => 'General Error: A message has been dropped, you are exceeding the server speed limit',
950 => 'Chat Error: Chat in $ERR_ARG is unavailable',
960 => 'IM and Info Error: You are sending messages too fast to $ERR_ARG',
961 => 'IM and Info Error: You missed an IM from $ERR_ARG because it was too big',
962 => 'IM and Info Error: You missed an IM from $ERR_ARG because it was sent too fast',
970 => 'Dir Error: Failure',
971 => 'Dir Error: Too many matches',
972 => 'Dir Error: Need more qualifiers',
973 => 'Dir Error: Dir service temporarily unavailble',
974 => 'Dir Error: Email lookup restricted',
975 => 'Dir Error: Keyword ignored',
976 => 'Dir Error: No keywords',
977 => 'Dir Error: Language not supported',
978 => 'Dir Error: Country not supported',
979 => 'Dir Error: Failure unknown $ERR_ARG',
980 => 'Auth Error: Incorrect nickname or password',
981 => 'Auth Error: The service is temporarily unavailable',
982 => 'Auth Error: Your warning level is too high to sign on',
983 => 'Auth Error: You have been connecting and disconnecting too frequently. Wait 10 minutes and try again.',
989 => 'Auth Error: An unknown signon error has occurred $ERR_ARG' );
Next, we intitiate a null $socket variable to serve as an output buffer for calls and responses to the TOC host server, and a $clientSequenceNumber variable to help pack in sockets in an orderly fashion for sending.
We also need the Host and Port connections for AIM's TOC protocol. These are pretty standard and never change - but never say never, I guess.
$socket = null;
$clientSequenceNumber = 0;
$tocHost = "toc.oscar.aol.com";
$tocPort = 5190;
$authHost = "login.oscar.aol.com";
$authPort = 5159;
Finally, the few user-defined variables. $screenName and $password are those of your bot. (In order for these to work, you will have to actually have a working AIM account.) The $controller variable is optional but highly recommended. Your bot will be able to identify who is talking to it, so you can have "admin" commands via your username, such as the ability to sign the bot off, go idle, or warn people vicariously.
$screenName="MyBot";
$password = "MyPassword";
$controller="MyAIMName";
Step 3: Generating Read, Write, and Sign On Functions
Most of the readFlap(), sendFlap(), and sendRaw() functions were created both trial-and-error - sending a response and seeing what comes back - and simplifying the input/output mechanism. Essentially they take in a particular TOC protocol command - "toc_signon" or "toc_sendim", to name the most common ones - a screenname, and any optional arguments, such as a message or a password. These get passed on to the TOC system, which interprets them correctly and generates the appropriate AIM response.
In other words, you can learn a lot from these, but you should just copy them and let it go.
(Perhaps the only interesting command here is the RoastPassword() function, which uses a very transparent encoding function that is initiated by the TOC serves themselves. I wonder if this easily reverse engineered. I doubt it, I guess.)
function readFlap() {
global $socket;
$header = fread($socket, SFLAP_HEADER_LEN);
if(strlen($header) < SFLAP_HEADER_LEN)
return(0);
$headerArray = unpack ("aasterisk/CframeType/nsequenceNumber/ndataLength", $header);
$packet = fread($socket, $headerArray['dataLength']);
if($headerArray['frameType'] == SFLAP_TYPE_SIGNON)
$packetArray = unpack("Ndata", $packet);
else
$packetArray = unpack("a*data", $packet);
$data = array_merge($headerArray, $packetArray);
return $data;
}
function sendRaw($data) {
global $socket;
if(fwrite($socket, $data) == FALSE)
return 0;
return 1;
}
function sendFlap($frameType, $data) {
global $socket, $clientSequenceNumber;
if( strlen($data) &rt; SFLAP_MAX_LENGTH)
{
$data = substr($data, 0, (SFLAP_MAX_LENGTH-2) );
$data .= '"';
}
$data = rtrim($data);
$data .= "\0";
$header = pack("aCnn", '*', $frameType, $clientSequenceNumber, strlen($data));
$packet = $header . $data;
if(fwrite($socket, $packet) == FALSE)
die("Unable to send the packet.<BR>\n");
$clientSequenceNumber++;
return $packet;
}
function sendTocFlapSignon() {
global $screenName;
$data = pack("Nnna".strlen($screenName), 1, 1, strlen($screenName), $screenName);
$result = sendFlap(SFLAP_TYPE_SIGNON, $data);
return $result;
}
function RoastPassword($password) {
$roastString = 'Tic/Toc';
$roastedPassword = '0x';
for ($i = 0; $i < strlen($password); $i++)
$roastedPassword .= bin2hex($password[$i] ^ $roastString[($i % 7)]);
return $roastedPassword;
}
function tocSignon() {
global $screenName, $password;
global $authHost, $authPort;
$roastedPassword = RoastPassword($password);
$tocSignon = 'toc_signon '.$authHost.' '.$authPort.' '.$screenName. ' '.$roastedPassword;
$tocSignon .= ' "english" "AOLIM:\$Version 1.1\$"'."\0";
$result = sendFlap(SFLAP_TYPE_DATA, $tocSignon);
return $result;
}
function normalize($screenName) {
return eregi_replace("[[:space:]]+","",strtolower($screenName));
}
function signOn() {
global $screenName, $roastedPassword, $socket, $clientSequenceNumber, $ERROR_MSGS;
global $tocHost, $tocPort;
echo "Signing on using $screenName<BR>\n";
if( !($socket = fsockopen($tocHost, $tocPort, $errorno, $errorstr, 30)) )
die("$errorstr ($errorno)");
echo "Connected to the server<BR>\n";
if( !sendRaw("FLAPON\r\n\r\n") )
die("Error sending the FLAPON packet.");
echo "FLAPON packet sent<BR>\n";
if( !( $result = readFlap() ) )
die("No response from server.<BR>\n");
if($result['asterisk']!='*' && $result['frameType']!=1 && $result['dataLength']!=4 && $result['data']!=1)
die("Invalid FLAP SIGNON response from the server.<BR>\n");
sendTocFlapSignon();
tocSignon();
$result = readFlap();
if($result['asterisk'] != '*' && $result['frameType'] != 2 )
die("Invalid response from server.<BR>\n");
if($result['data'] == "SIGN_ON:TOC1.0")
echo "toc_signon success SIGN_ON <br>";
else if(substr($result['data'], 0, 6) == "ERROR:")
{
$where = strpos( $result['data'], ":");
die($ERROR_MSGS[chop(substr($result['data'], $where+1))] . "<br>\n");
}
sendFlap(SFLAP_TYPE_DATA, "toc_add_permit");
sendFlap(SFLAP_TYPE_DATA, "toc_add_deny");
$tocAddBuddy = "toc_add_buddy " . $screenName;
sendFlap(SFLAP_TYPE_DATA, $tocAddBuddy);
echo $screenName." Added";
sendFlap(SFLAP_TYPE_DATA, "toc_init_done");
echo "sent toc_init_done<BR>\n";
}
Step 4: Generating Responses
Okay, so now that all the functions are in place, we need to set up a virtual infinite loop to constantly read data into our PHP script. (It's virtual because you can use your "admin" powers to end the loop and sign the bot off.)
Then the loops is simple. First you use readFlap() and assign the result to $result. Then you can use the explode() function in PHP to divide the data up into a nickname (the sender) and the message. From here you can manipulate the data as much as you want.
With the code I've created, there are two primary ways of generating responses. The first is to use a user-defined array of "buzzwords" and the responses to those words. Check the user's message against the buzzwords, and post the matching response. Bingo. The array method would be better suited for a simpler bot, or if there is only one specific purpose. This would also be ideal if you were populating your responses based on constantly regenerating data, such as a stock price. You could set it up so that the program could open up an XML file of stock prices and refresh its array every time it receives a request, or automatically every 60 seconds, or some other method. This is left up to you.
The second method is to have a database of input and output to compare. This would be more in tune with either a very context-sensitive bot - for example, you could set up a whole text-based adventure a la Zork using a database and an AIM bot - or for a bot where you are supplying more than information. One use of a bot that I've enjoyed was a database search of my personal mp3s. After finding an mp3, I could enter an e-mail address and it would send the mp3 as an attachment. This was more efficient than memorizing my IP address or relying on a shaky webhost to have access to my files.
The code below accommodates both methods. First it searches the $buzzwords array and, finding no matches, moves on to the assigned $chosenDB to search it. This code is fairly generalized, and a certain knowledge about mySQL connections and SQL itself are necessary to generate responses correctly. But this code is also highly extensible, and frankly almost anything is possible within this rubric.
signOn();
$x=false;
while($x==false) {
$result = readFlap(); //grab the data
if(ereg('^IM_IN',$result['data'])) {
$nick = explode(':',$result['data']);
$nick = normalize($nick[1]);
$buzzwords[0]="hello";
$buzzwords[1]="shit";
$buzzwords[2]="fuck";
$response[0]='toc_send_im ' . $nick . ' "Hello! How are you?"';
$response[1]='toc_evil ' . $nick . ' norm';
$response[2]='toc_evil ' . $nick . ' norm';
$keepsearching=true;
foreach($buzzwords as $key => $value) {
if(ereg($value,$result['data'])) {
sendFlap(SFLAP_TYPE_DATA,$response[$key]);
$keepsearching=false;
}
}
if ($keepsearching) {
$nick = explode(':',$result['data']);
$search_result=strip_tags($nick[3]);
$nick = normalize($nick[1]);
$dbpass="PASSWORD";
$chosenDB="DATABASE";
mysql_connect("localhost","root",$dbpass);
mysql_select_db($selectedDB);
$SQL="Select Response FROM Responses WHERE Input='$search_result'";
$query=mysql_query($SQL);
$feedback="";
list($response) = mysql_fetch_row($query);
sendFlap(SFLAP_TYPE_DATA,'toc_send_im ' . $nick . ' "'.$response.'"');
}
}
}
Step 5: Putting it All Together
The last step is to create a batch file to run the PHP code on a whim. It's pretty simple - just one line:
c:/php/php.exe -q c:/code_directory/aimbot.php
where you should insert the actual location of your php.exe file and the actual location of your aimbot.php file (or whatever you choose to name it).
Then just run that batch file whenever you want - when you close the batch file, you kill the bot. Fairly simple, and easy to manage. Enjoy the code!
<?php
define("MAX_PACKLENGTH",65535);
define("SFLAP_TYPE_SIGNON",1);
define("SFLAP_TYPE_DATA",2);
define("SFLAP_TYPE_ERROR",3);
define("SFLAP_TYPE_SIGNOFF",4);
define("SFLAP_TYPE_KEEPALIVE",5);
define("SFLAP_MAX_LENGTH",4096);
define("SFLAP_SUCCESS",0);
define("SFLAP_ERR_UNKNOWN",1);
define("SFLAP_ERR_ARGS",2);
define("SFLAP_ERR_LENGTH",3);
define("SFLAP_ERR_READ",4);
define("SFLAP_ERR_SEND",5);
define("SFLAP_FLAP_VERSION",1);
define("SFLAP_TLV_TAG",1);
define("SFLAP_HEADER_LEN",6);
ini_set("max_execution_time", "0");
$ERROR_MSGS = array ( 0 => 'Success',
1 => 'AOLIM Error: Unknown Error',
2 => 'AOLIM Error: Incorrect Arguments',
3 => 'AOLIM Error: Exceeded Max Packet Length (1024)',
4 => 'AOLIM Error: Reading from server',
5 => 'AOLIM Error: Sending to server',
6 => 'AOLIM Error: Login timeout',
901 => 'General Error: $ERR_ARG not currently available',
902 => 'General Error: Warning of $ERR_ARG not currently available',
903 => 'General Error: A message has been dropped, you are exceeding the server speed limit',
950 => 'Chat Error: Chat in $ERR_ARG is unavailable',
960 => 'IM and Info Error: You are sending messages too fast to $ERR_ARG',
961 => 'IM and Info Error: You missed an IM from $ERR_ARG because it was too big',
962 => 'IM and Info Error: You missed an IM from $ERR_ARG because it was sent too fast',
970 => 'Dir Error: Failure',
971 => 'Dir Error: Too many matches',
972 => 'Dir Error: Need more qualifiers',
973 => 'Dir Error: Dir service temporarily unavailble',
974 => 'Dir Error: Email lookup restricted',
975 => 'Dir Error: Keyword ignored',
976 => 'Dir Error: No keywords',
977 => 'Dir Error: Language not supported',
978 => 'Dir Error: Country not supported',
979 => 'Dir Error: Failure unknown $ERR_ARG',
980 => 'Auth Error: Incorrect nickname or password',
981 => 'Auth Error: The service is temporarily unavailable',
982 => 'Auth Error: Your warning level is too high to sign on',
983 => 'Auth Error: You have been connecting and disconnecting too frequently. Wait 10 minutes and try again.',
989 => 'Auth Error: An unknown signon error has occurred $ERR_ARG' );
$socket = null;
$clientSequenceNumber = 0;
$tocHost = "toc.oscar.aol.com";
$tocPort = 5190;
$authHost = "login.oscar.aol.com";
$authPort = 5159;
$screenName="MyBot";
$password = "MyPassword";
$controller="MyAIMName";
function readFlap() {
global $socket;
$header = fread($socket, SFLAP_HEADER_LEN);
if(strlen($header) < SFLAP_HEADER_LEN)
return(0);
$headerArray = unpack ("aasterisk/CframeType/nsequenceNumber/ndataLength", $header);
$packet = fread($socket, $headerArray['dataLength']);
if($headerArray['frameType'] == SFLAP_TYPE_SIGNON)
$packetArray = unpack("Ndata", $packet);
else
$packetArray = unpack("a*data", $packet);
$data = array_merge($headerArray, $packetArray);
return $data;
}
function sendRaw($data) {
global $socket;
if(fwrite($socket, $data) == FALSE)
return 0;
return 1;
}
function sendFlap($frameType, $data) {
global $socket, $clientSequenceNumber;
if( strlen($data) > SFLAP_MAX_LENGTH)
{
$data = substr($data, 0, (SFLAP_MAX_LENGTH-2) );
$data .= '"';
}
$data = rtrim($data);
$data .= "\0";
$header = pack("aCnn", '*', $frameType, $clientSequenceNumber, strlen($data));
$packet = $header . $data;
if(fwrite($socket, $packet) == FALSE)
die("Unable to send the packet.<BR>\n");
$clientSequenceNumber++;
return $packet;
}
function sendTocFlapSignon() {
global $screenName;
$data = pack("Nnna".strlen($screenName), 1, 1, strlen($screenName), $screenName);
$result = sendFlap(SFLAP_TYPE_SIGNON, $data);
return $result;
}
function RoastPassword($password) {
$roastString = 'Tic/Toc';
$roastedPassword = '0x';
for ($i = 0; $i < strlen($password); $i++)
$roastedPassword .= bin2hex($password[$i] ^ $roastString[($i % 7)]);
return $roastedPassword;
}
function tocSignon() {
global $screenName, $password;
global $authHost, $authPort;
$roastedPassword = RoastPassword($password);
$tocSignon = 'toc_signon '.$authHost.' '.$authPort.' '.$screenName. ' '.$roastedPassword;
$tocSignon .= ' "english" "AOLIM:\$Version 1.1\$"'."\0";
$result = sendFlap(SFLAP_TYPE_DATA, $tocSignon);
return $result;
}
function normalize($screenName) {
return eregi_replace("[[:space:]]+","",strtolower($screenName));
}
function signOn() {
global $screenName, $roastedPassword, $socket, $clientSequenceNumber, $ERROR_MSGS;
global $tocHost, $tocPort;
echo "Signing on using $screenName<BR>\n";
if( !($socket = fsockopen($tocHost, $tocPort, $errorno, $errorstr, 30)) )
die("$errorstr ($errorno)");
echo "Connected to the server<BR>\n";
if( !sendRaw("FLAPON\r\n\r\n") )
die("Error sending the FLAPON packet.");
echo "FLAPON packet sent<BR>\n";
if( !( $result = readFlap() ) )
die("No response from server.<BR>\n");
if($result['asterisk']!='*' && $result['frameType']!=1 && $result['dataLength']!=4 && $result['data']!=1)
die("Invalid FLAP SIGNON response from the server.<BR>\n");
sendTocFlapSignon();
tocSignon();
$result = readFlap();
if($result['asterisk'] != '*' && $result['frameType'] != 2 )
die("Invalid response from server.<BR>\n");
if($result['data'] == "SIGN_ON:TOC1.0")
echo "toc_signon success SIGN_ON
";
else if(substr($result['data'], 0, 6) == "ERROR:")
{
$where = strpos( $result['data'], ":");
die($ERROR_MSGS[chop(substr($result['data'], $where+1))] . "<br>\n");
}
sendFlap(SFLAP_TYPE_DATA, "toc_add_permit");
sendFlap(SFLAP_TYPE_DATA, "toc_add_deny");
$tocAddBuddy = "toc_add_buddy " . $screenName;
sendFlap(SFLAP_TYPE_DATA, $tocAddBuddy);
echo $screenName." Added";
sendFlap(SFLAP_TYPE_DATA, "toc_init_done");
echo "sent toc_init_done<BR>\n";
}
signOn();
$x=false;
while($x==false) {
$result = readFlap(); //grab the data
if(ereg('^IM_IN',$result['data'])) {
$nick = explode(':',$result['data']);
$nick = normalize($nick[1]);
$buzzwords[0]="hello"; // generic test response
$buzzwords[1]="shit";
$buzzwords[2]="fuck"; // warn 'em for bad words
$response[0]='toc_send_im ' . $nick . ' "Hello! How are you?"';
$response[1]='toc_evil ' . $nick . ' norm';
$response[2]='toc_evil ' . $nick . ' norm';
$keepsearching=true;
foreach($buzzwords as $key => $value) {
if(ereg($value,$result['data'])) {
sendFlap(SFLAP_TYPE_DATA,$response[$key]);
$keepsearching=false;
}
}
if ($keepsearching) {
$nick = explode(':',$result['data']);
$search_result=strip_tags($nick[3]);
$nick = normalize($nick[1]);
$dbpass="PASSWORD";
$chosenDB="DATABASE";
mysql_connect("localhost","root",$dbpass);
mysql_select_db($chosenDB);
$SQL="Select Response FROM Responses WHERE Input='$search_result'";
$query=mysql_query($SQL);
list($response) = mysql_fetch_row($query);
sendFlap(SFLAP_TYPE_DATA,'toc_send_im ' . $nick . ' "'.$response.'"');
}
}
}
?>