Creating an AIM Lite Plugin to Receive and Save a Text File

AIM Lite Plugin

by Doug Schwartz
5/16/2007

Introduction

In my last article, Creating an AIM Lite Plugin, I discussed how you can create a simple plugin for AIM Lite. I'd been wracking my brain about what to do next, when I ran across a question in the forum about sending a text file. So I decided I would try to create a simple plugin that lets you send a text file--as a specially formatted IM--to a buddy, and also lets your buddy receive the text file and save it (provided that they also have the plugin).

Well, that didn't work. Creating one plugin to do two tasks turned out to be more complex than necessary. So I thought, why one plugin anyway? It's really two tasks--one to send the file and one to receive the file. So I split my work up and created two plugins.

As I was creating the plugins, I discovered I could construct the IM that I was using to send the text file in such a manner that I could test my receiving plugin without the sending plugin. This is very important, as it reduced my debugging efforts considerably. I knew that once I got the receiving plugin working as I wanted, I could then focus all of my energy on the sending plugin. I'll let you decide if I was right.

So here's the deal. This article covers the second plugin, which receives a text file encapsulated in an IM and saves it to the receiver's computer, if they want to save it. The article that follows this one ("Creating an AIM Lite Plugin to Sent a Text File") covers the sending plugin.

If you have not created an AIM Lite plugin, or have not read my last article, shame on you! Just kidding. If you have neither created an AIM Lite plugin nor read my last article to understand the basics, the following section, which is a brief introduction to AIM Plugins, will provide you with the basic knowledge you'll need as you read the remainder of this article.

AIM Plugin Basics

The AIM Plugins page on the developer.aim.com site is the starting point for AIM plugin documentation. The basic steps for creating an AIM Lite plugin are:

  1. Download and install AIM Lite.
  2. Get an account on the AIM Developer website.
  3. Get a developer key for your plugin. To get a key, go to the AIM Plugin Registration web page and follow the instructions. You will need the key value to run your plugin.

Plugins are deployed as an AWI file, which is a ZIP file with the extension altered to ".awi". AWI files typically contain at least three files:

  • A plugin.xml metadata file
  • A content directory with a main.box XML file.
  • A JavaScript file with a .js extension that is referenced in main.box.

AWI files are extremely easy to install. Once someone has AIM Lite and the AWI file, they double-click the AWI file, and voila! The plugin is installed.

Creating the Receive/Save Text File Plugin

The receiving plugin is designed to intercept the IM before the user sees it and determine whether the IM contains an encapsulated text file, and if it does, present a save file dialog box so the user can save the text file to their computer.

We are going to create the plugin in the following order:

  1. Create the plugin.xml file.
  2. Create the main.box file.
  3. Create the main.js file.

Creating the plugin.xml File

The plugin.xml file contains a single widget element. This element can contain a number of sub-elements, but the only required sub-element is the uuid, which is a developer or deployment key. Replace YOUR_DEVELOPER_KEY with your actual developer key in the following code.

<?xml version="1.0" encoding="utf-8" ?>
<widget
    uuid = "{YOUR_DEVELOPER_KEY}"
    version = "1.0"
    name = "ReceiveTextFile"
    description = "This plugin receives a text file"
    vendor = "Me, Myself, and I Inc."
/>

Creating the main.box File

The main.box file contains a single window element. This element defines the user interface (UI) for the plugin. A plugin can be visible, invisible (no UI), or only visible in response to an event. The plugin is invisible, and main.box contains the following text:

<?xml version="1.0" encoding="utf-8" ?>
<window
    xmlns="http://www.aol.com/boxely/box.xsd"
    xmlns:s="http://www.aol.com/boxely/style.xsd"
    xmlns:on="http://www.aol.com/boxely/reaction.xsd"
    on:constructed="onConstructed();"
    on:destroyed="onDestroyed();"
    s:height="0"
    s:width="0"
    hidden="true"
    collapsed="true"
    floating="true"
>
  <code id="main" language="jscript" src="main.js"/>
</window>

You can easily tell the plugin is invisible by the hidden="true" element.

Let's deconstruct this XML line by line.

  1. Defines the text encoding.
  2. Creates the opening window element tag.
  3. Creates the default namespace for this window element. Namespaces are a handy way to save a boatload of typing in an XML file.
  4. Creates the s namespace for this window element.
  5. Creates the on namespace for this window element.
  6. Identifies onConstructed as the function to which the constructed event notification is sent.
    This event is fired whenever the plugin is created. onConstructed should create any resources that are needed for the plugin.
  7. Identifies onDestroyed as the function to which the destructed event notification is sent.
    This event is fired whenever the plugin is destroyed. onDestructed should release any resources that were created in the onConstructed function.
  8. Defines the height of the window. Since this window is not displayed, we set this to 0.
  9. Defines the width of the window. Since this window is not displayed, we set this to 0.
  10. Defines whether the window is not visible.
  11. Defines whether the window is collapsed.
  12. Defines whether the window can be moved.
  13. Closes the window element attributes.
  14. Defines the JavaScript file where the code resides.
  15. Closes the window element.

Creating the main.js File

The main.js file contains the following sections:

  • Three global variables.
  • The onConstructed function that is called when the plugin is created.
  • The onDestroyed function that is called when the plugin is finished.
  • The BeforeImReceived event handler that is called before the message is received. This is where we examine the IM to determine whether it contains a sent text file.
  • The commandListener functions that detect whether the plugin has been invoked.

The first line of main.js contains a global variable, kCommandId, which defines the integer value of the plugin command. This code is as follows:

var kCommandId = 0;

All AIM functionality, including user preferences, is accessed through a session object. This object is an implementation of the IAccSession interface, and is accessed through the scene.paramsDictionary property's valueForKey function using the session key.

Information about a plugin is accessed through a pluginInfo object. This object is an implementation of the IAccPluginInfo interface, and is accessed through the scene.paramsDictionary property's valueForKey function using the pluginInfo key.

Once we have the pluginInfo object, we can create the plugin command using the pluginInfo object's addCommand function, and assign the text we present to the user with the text property of the command.

Event handling begins with a call to the scene.connectObject function. The first parameter is the IAccSession object, and the second parameter is a prefix to be used when defining event callbacks. The plugin uses the session_ prefix; therefore, the handler for the DAccEvents::BeforeImSend event is session_BeforeImSend.

Adding commands is done as with COM plugins, using the IAccPluginInfo::AddCommand function. To receive commands from the application, the plugin must create a commandListener object that implements the QueryStatus and Exec methods, and set this object to the commandTarget key of the supplied paramsDictionary.

Here is the onConstructed function:

function onConstructed() {
   var dict = scene.paramsDictionary;
   var session = dict.valueForKey("session");

   var pInfo = dict.valueForKey("pluginInfo");
   var cmd = pInfo.addCommand(kCommandId);
   cmd.text = "Receive text file";

   scene.connectObject(session, "session_");
   dict.setValueForKey(new commandListener(),
                       "commandTarget");
}

The onDestroyed function is simple; it deletes the association to the session.

function onDestroyed() {
   scene.disconnectObject("session_");
}

The onConstructed function created an association between the session object and session_. We use that association to trap the IM message before it is received in the BeforeImReceived event handler. The BeforeImReceived event handler does the following:

  1. Creates a copy of the IM so it can restore any IM that does not contain a text file.
  2. Converts the IM to plain text.
  3. Splits the message text into parts by the colon delimiter.
  4. If the message does not contain three parts, restores the IM and returns.
  5. Decodes the message.
  6. Compares the resulting size of the message content with the value of the second part of the message.
  7. If the values do not agree, closes the plugin.
  8. Opens the Save File dialog box to save the file contents.
  9. Sets the IM text to an empty string.

Here is the BeforeImReceived event handler.

function session_BeforeImReceived(session,
                                  imSession,
                                  sender,
                                  im) {
   // Save IM in case it isn't one containing the file
   var tmpIm = im;

   // Convert IM to plain text, in place
   im.ConvertToMimeType("text/plain");
   var imText = im.Text;

   var contents = imText.split(":");
    
   if (contents.length != 3) {
      im = tmpIm;
      return;
   }
    
   var filename     = contents[0];
   var filesize     = parseInt(contents[1]);
   var fileContents = contents[2];

   // Contents is encoded on sending side
   fileContents = decodeURIComponent(fileContents);

   if (filesize != fileContents.length) {
      this.scene.close(this.scene);
   }

   // Try to save as the same filename
   var filter = "Text Files (*.txt)|*.txt||";
   var fileList = appUtils.saveFileDialog(this.scene,
                                          "Save File",
                                          filename,
                                          null,
                                          filter,
                                          "txt");

   if (fileList) {
      // File list count should come back 1.
      // Can only select 1 file when saving.
      var count = fileList.count;

      if (count == 1) {
         var myFile = fileList.getValue(0);

         var basics = shell.serviceManager.basics;
         var fs = basics.fileStream;
         var sw = basics.rawStreamWriter;

         fs.openForWrite(myFile, true, true);
         sw.stream = fs;

         sw.writeString(fileContents, "");
         fs.close();

         // Consume IM
         im.Text = "";
      }
   }
}

We also need to create the QueryStatus and Exec functions. Here is the code for these functions.

function commandListener() {}

commandListener.prototype.QueryStatus=function(id,
                                               users) {
   return (id == kCommandId) ? true : false;
}

commandListener.prototype.Exec=function(id, users) {
   if (id == kCommandId) {
      // Do nothing here
      // Everything happens in the event handler
   }
}

Testing the Plugin

Once you have created the plugin, perform the following steps to test the plugin:

  1. Zip up the three files, plugin.xml, content/main.box, and content/main.js, into a ZIP file.
  2. Change the extension to ".awi".
  3. Double-click the AWI file to install the plugin.
  4. Fire up two separate AIM Lite clients
    (this is easy to do and I will explain it in just a moment).
  5. Create an IM session between them.
  6. Enable the plugin in one of the clients.
  7. Send the following IM from the other client:
    test.txt:14:This is a test
    .

The plugin then presents a dialog box to save the file test.txt. Save it and then open it and make sure it contains the text This is a test.

Figure 1 shows an example of the Save File dialog box.

The Save File dialog box
Figure 1. The Save File dialog box

Bugs, Bugs, Everywhere

If you have been writing code for any time at all, you will encounter bugs. Since we are working with XML and JavaScript, let me give you a couple of hints that might make your life easier. I use Visual Studio with my XML files. I cannot tell you how many times I have left a quote open or forgot to close a tag when using just a simple text editor.

Similarly, I discovered I had left out a closing brace in a JavaScript file. I only found my mistake when I fired up NetBeans and reformatted the source. As I scrolled down, I discovered the last line was not flush to the left margin. Hello! Problem found! Once I put the closing brace in, my plugin worked like a charm (not really, but at least that part was fixed).

Another hugely important tip to help you find bugs in your JavaScript is to find the last place your code was working correctly, and add a debugger; statement. When the statement is encountered, the plugin breaks into the debugger. Once again I use Visual Studio, but there are others available, depending upon the software on your computer.

Firing Up Separate AIM Lite Clients

One very helpful tip I found in the AIM Lite FAQ is how to create two AIM Lite clients so you can create an IM session on one computer. Just open a command window, navigate to the AIM Lite folder (typically C:\Program Files\AIM Lite), and issue the following command for each client:

aimlite.exe -standalone

Conclusion

Now you understand how to receive an encapsulated text file as an IM. The next article explains how to encapsulate a text file as an IM and send it to your buddy. Of course, your buddy must install the receiving plugin for this process to work.

Resources

ReceiveTextFile.zip contains the complete source code for this article. If you want to see the plugin at work:

  1. Install AIM Lite.
  2. Unzip ReceiveTextFile.zip to get the source files for the receiving plugin.
  3. Open the plugin.xml file.
  4. Change YOUR_DEVELOPER_KEY to your developer key.
  5. Zip the files back up.
  6. Change the file extension to ".awi".
  7. Double-click the AWI file to install the plugin.

References

Here are a number of web pages where you can get further information about AIM Lite, plugins, and the source code for this article.

 


Enable the Subscriptions block here!