Perl Scripting HOWTO

Introduction

libpurple Perl Plugins are set up very similarly to their C counterparts. Most of the API calls are implemented and are divided into packages. There are some significant differences between the Perl and C API. Much like the C API, the best place to seek guidance is the source, which is located in the *.xs files in the [libpurple|pidgin]/plugins/perl/common directories. The tutorial that follows will be example based and attempt to touch on some of the notable features of the embedded perl interpreter. It is also important to note that some of the C API is missing in libpurple's perl API.

It is possible to get Gtk2-Perl to work with libpurple's Perl API, however you must not load the module with use, but rather with require. Keep this in mind if you would like to use other perl modules that are dynamically loaded. You can avoid the problem by always using require instead of use.

Writing your first script

Let us start with a simple example of a perl plugin. The following code sample is a complete plugin that can be copied and used as-is.

use Purple;
%PLUGIN_INFO = (
perl_api_version => 2,
name => "Perl Test Plugin",
version => "0.1",
summary => "Test plugin for the Perl interpreter.",
description => "Your description here",
author => "John H. Kelm <johnhkelm\@gmail.com",
url => "http://pidgin.im",
load => "plugin_load",
unload => "plugin_unload"
);
sub plugin_init {
return %PLUGIN_INFO;
}
sub plugin_load {
my $plugin = shift;
Purple::Debug::info("testplugin", "plugin_load() - Test Plugin Loaded.\n");
}
sub plugin_unload {
my $plugin = shift;
Purple::Debug::info("testplugin", "plugin_unload() - Test Plugin Unloaded.\n");
}

It is necessary to load the libpurple perl package with the

use Purple;

line; this makes all the libpurple perl API available to the script. The %PLUGIN_INFO hash contains all the information that will be displayed in the Plugin frame of the Preferences dialog, it also contains information about how the plugin is to be handled. The load and unload keys specify subroutines to be called when the plugin is loaded and unloaded. There are other key values that may be present in the %PLUGIN_INFO hash; some of those will be covered in the following sections.

The Perl subroutine plugin_init is executed when the plugin is probed by the plugin subsystem. What this means is that as soon as Pidgin or Finch is started, this subroutine is run once, regardless of whether or not the plugin is actually loaded. The other two subroutines present are the load and unload routines specified in the %PLUGIN_INFO hash and will receive the plugin handle as an argument. When the plugin is loaded and subsequently unloaded, it will print a message to the debug window using the Purple::Debug::info() API call.

In order to use this simple plugin, save the script text in a file with a .pl extension in your ~/.purple/plugins directory. After restarting Pidgin, you should see the plugin ("Perl Test Plugin") listed in the plugin list under "Tools -> Plugins". To view the debug output, make sure you run Pidgin from the console with the '-d' flag or open the Debug Window which is available in the "Help" menu. When you check the checkbox next the plugin, you should see a message appear in the Debug Window (or console); similarly, when you uncheck the checkbox you should see another message appear. You have now created a framework that will allow you to create almost any kind of libpurple plugin you can imagine.

Account and Account Option Functions

The Account API is in the Purple::Account:: and Purple::Accounts:: packages; both are nearly identical to their C counterparts, purple_account_ and purple_accounts_. The Account Option API is in the Purple::Account::Option package and is identical to the C implementation, purple_account_option.

The Account* APIs allow scripts to create, remove, and edit accounts. An account will have the Perl type of "PurpleAccount". (Note: Purple types have no real meaning in perl scripts, other than that the types passed to perl subroutines need to be correct.) This section will not go into detail about the Purple::Account::Option package since its use is mainly in building protocol plugins, which are outside the scope of this document. However, the API for the Purple::Account::Option package should function as expected, should you need to use it.

To reduce redundant code, the following code examples will use the simple plugin from the previous section as a template. To highlight some of the more useful features of the Account API, we will be replacing the plugin_load perl subroutine. For testing purposes, we will display output on the command line by using perl print commands.

sub plugin_load {
$plugin = shift;
# Testing was done using AIM, but this should work regardless of the protocol chosen
my $protocol = "prpl-aim";
my $account_name = "test";
# Create a new Account
print "Testing: Purple::Account::new()... ";
$account = Purple::Account->new($account_name, $protocol);
if ($account) { print "ok.\n"; } else { print "fail.\n"; }
# Add a new Account
print "Testing: Purple::Account::add()...";
Purple::Accounts::add($account);
print "pending find...\n";
# Find the account we just added to verify its existence
print "Testing: Purple::Accounts::find()...";
$account = Purple::Accounts::find($account_name, $protocol);
if ($account) { print "ok.\n"; } else { print "fail.\n"; }
# Return the username
print "Testing: Purple::Account::get_username()... ";
$user_name = $account->get_username();
if ($user_name) {
print "Success: $user_name.\n";
} else {
print "Failed!\n";
}
# Verify if the user is connected
print "Testing: Purple::Account::is_connected()";
if ($account->is_connected()) {
print " Connected.\n";
} else {
print " Disconnected.\n";
}
# The status mechanism is how users are Connected, set Away,
# Disconnected (status set to Offline), etc
# $status is now a Purple::Status perl type.
print "Testing: Purple::Accounts::get_active_status()... ";
if ($account->get_active_status()) {
print "Okay.\n";
} else {
print "Failed!\n";
}
# It follows that to connect a user you must set the account status to
# "available" similarly we can disconnect a user by setting the account
# status to "offline"
print "Testing: Purple::Accounts::connect()...pending...\n";
$account->set_status("available", TRUE);
$account->set_enabled(Purple::Core::get_ui(), TRUE);
$account->connect();
}

The above code is mostly explained in the comments, however there are a few notable points to make. The variables above are all specialized Perl types that contain pointers to the actual Purple types. They can be reassigned at will, just like any other variable in Perl. The only way to edit the internal values of a Purple type from within perl is to use the accessor methods, e.g. Purple::Account::get_username(). If you would like to assign a C NULL value, simply use undef.

Buddylist, Group and Chat API

The BuddyList, Group and Chat APIs are very similar and whatever is shown for the Purple::BuddyList API should carry over to Purple::BuddyList::Chat and Purple::BuddyList::Group. Note that a Purple::Find package was created to keep the naming consistent with the C API.

sub plugin_load {
my $plugin = shift;
my $protocol = "prpl-aim";
my $account_name = "test";
# This is how we get an account to use in the following tests. You should replace the username
# with an existing user
$account = Purple::Accounts::find($account_name, $protocol);
# Testing a find function: Note Purple::Find not Purple::Buddy:find!
# Furthermore, this should work the same for chats and groups
Purple::Debug::info("testplugin", "Testing: Purple::Find::buddy()...");
$buddy = Purple::Find::buddy($account, "BUDDYNAME");
Purple::Debug::info("", ($buddy ? "ok." : "fail.") . "\n");
# If you should need the handle for some reason, here is how you do it
Purple::Debug::info("testplugin", "Testing: Purple::BuddyList::get_handle()...");
$handle = Purple::BuddyList::get_handle();
Purple::Debug::info("", ($handle ? "ok." : "fail.") . "\n");
# This gets the Purple::BuddyList and references it by $blist
Purple::Debug::info("testplugin", "Testing: Purple::get_blist()...");
$blist = Purple::get_blist();
Purple::Debug::info("", ($blist ? "ok." : "fail.") . "\n");
# This is how you would add a buddy named "NEWNAME" with the alias "ALIAS"
Purple::Debug::info("testplugin", "Testing: Purple::BuddyList::Buddy::new...");
$buddy = Purple::BuddyList::Buddy::new($account, "NEWNAME", "ALIAS");
Purple::Debug::info("", ($buddy ? "ok." : "fail.") . "\n");
# Here we add the new buddy '$buddy' to the group "GROUP"
# so first we must find the group
Purple::Debug::info("testplugin", "Testing: Purple::Find::group...");
$group = Purple::Find::group("GROUP");
Purple::Debug::info("", ($group ? "ok." : "fail.") . "\n");
# To add the buddy we need to have the buddy, contact, group and node for insertion.
# For this example we can let contact be undef and set the insertion node as the group
Purple::Debug::info("testplugin", "Testing: Purple::BuddyList::add_buddy...\n");
Purple::BuddyList::add_buddy($buddy, undef, $group, $group);
# The example that follows gives an indication of how an API call that returns a list is handled.
# In this case the buddies of the account found earlier are retrieved and put in an array '@buddy_array'
# Further down an accessor method is used, 'get_name()' -- see source for details on the full set of methods
Purple::Debug::info("testplugin", "Testing: Purple::Find::buddies...\n");
@buddy_array = Purple::Find::buddies($account, undef);
if (@buddy_array) {
Purple::Debug::info("testplugin", "Buddies in list (" . @buddy_array . "): \n");
foreach $bud (@buddy_array) {
Purple::Debug::info("testplugin", Purple::BuddyList::Buddy::get_name($bud) . "\n");
}
}
}

The BuddyList API allows plugins to edit buddies in the list, find the buddies for a given account, assign aliases, and further manipulate the structure as needed. It is also contains methods for accessing Purple::BuddyList::Group and Purple::BuddyList::Chat types.

Connection API

The Purple::Connection API is one of the many packages that will not be covered in-depth in this tutorial. It is most useful to protocol plugin developers. However, the entire purple_connection_ API has corresponding, functioning perl subroutines.

Conversation API

The libpurple perl APIs for purple_conversation_ and pidgin_conv_window_ allow plugins to interact with existing conversations, create new conversations, and modify conversations at will. In the example script, a new window is created, displayed and a new conversation instant message is created. The following example again replaces the plugin_load subroutine in the simple plugin template. The Purple::Conversation::Chat package handles the purple_conv_chat_ portion of the API very similarly to how the Purple::Conversation::IM package is used in the examples that follow.

Notice that the interaction with the conversation window is in the Pidgin package as it is UI-specific code interacting with Pidgin and not libpurple. To use any of the Pidgin:: functionality, you will need to add the following to the top of your script:

use Pidgin;
sub plugin_load {
my $plugin = shift;
my $protocol = "prpl-aim";
my $account_name = "test";
$account = Purple::Accounts::find($account_name, $protocol);
# First we create two new conversations.
print "Testing Purple::Conversation->new()...";
$conv1 = Purple::Conversation->new(1, $account, "Test Conversation 1");
if ($conv1) { print "ok.\n"; } else { print "fail.\n"; }
print "Testing Purple::Conversation->new()...";
$conv2 = Purple::Conversation->new(1, $account, "Test Conversation 2");
if ($conv2) { print "ok.\n"; } else { print "fail.\n"; }
# Second we create a window to display the conversations in.
# Note that the package here is Pidgin::Conversation::Window
print "Testing Pidgin::Conversation::Window->new()...\n";
$win = Pidgin::Conversation::Window->new();
# The third thing to do is to move second the conversation to a new window.
# The subroutine add_gtkconv() returns the number of conversations
# present in the window.
print "Testing Pidgin::Conversation::Window::add_conversation()...";
$gtkconv2 = Pidgin::Conversation::get_gtkconv($conv2);
$gtkconv2->get_window()->remove_gtkconv($gtkconv2);
$conv_count = $win->add_gtkconv($gtkconv2);
if ($conv_count) {
print "ok..." . $conv_count . " conversations...\n";
} else {
print "fail.\n";
}
# Now the window is displayed to the user.
print "Testing Pidgin::Conversation::Window::show()...\n";
$win->show();
# Use get_im_data() to get a handle for the conversation
print "Testing Purple::Conversation::get_im_data()...\n";
$im = $conv1->get_im_data();
if ($im) { print "ok.\n"; } else { print "fail.\n"; }
# Here we send messages to the conversation
print "Testing Purple::Conversation::IM::send()...\n";
$im->send("Message Test.");
print "Testing Purple::Conversation::IM::write()...\n";
$conv2->get_im_data()->write("SENDER", "<b>Message</b> Test.", 0, 0);
}

The next block of code shows how a script can close a known conversation window $win.

print "Testing Pidgin::Conversation::Window::get_gtkconv_count()...\n";
$conv_count = $win->get_gtkconv_count();
print "...and it returned $conv_count.\n";
if ($conv_count > 0) {
print "Testing Pidgin::Conversation::Window::destroy()...\n";
$win->destroy();
}

Plugin Preference and Gtk Preference API

The plugin preference API allows plugins to display options for manipulating the plugin's behavior in a popup window. The method used for creating the pane in native C plugins does not allow a direct mapping into perl. Therefore, perl plugin writers must be aware that there will be significant differences in how they create plugin preference panes.

To first create a standard plugin preference tab, we need specify the prefs_info key/value pair in the %PLUGIN_INFO hash. This specifies the name of the perl subroutine that will build and return a Purple::Pref::Frame.

%PLUGIN_INFO = {
...,
prefs_info => "prefs_info_cb"
};

The perl subroutine prefs_info_cb will be called to create the preferences popup for the perl plugin. The following example will demonstrate creating a preference frame. It is necessary to first initialize the preferences to be used in the plugin_load subroutine, as follows.

sub plugin_load {
my $plugin = shift;
# This is necessary to create each level in the preferences tree.
Purple::Prefs::add_none("/plugins/core/perl_test");
# Here we are adding a set of preferences
# The second argument is the default value for the preference.
Purple::Prefs::add_bool("/plugins/core/perl_test/bool", 1);
Purple::Prefs::add_string("/plugins/core/perl_test/choice_str", "ch1");
Purple::Prefs::add_int("/plugins/core/perl_test/choice_int", 1);
Purple::Prefs::add_string("/plugins/core/perl_test/text", "Foobar");
}

Now we can provide an UI for manipulating these preferences in our prefs_info function.

sub prefs_info_cb {
# The first step is to initialize the Purple::Pref::Frame that will be returned
$frame = Purple::PluginPref::Frame->new();
# Create a new boolean option with a label "Boolean Label" and then add
# it to the frame
$ppref = Purple::PluginPref->new_with_label("Boolean Label");
$frame->add($ppref);
$ppref = Purple::PluginPref->new_with_name_and_label(
"/plugins/core/perl_test/bool", "Boolean Preference");
$frame->add($ppref);
# Create a set of choices. To do so, we must set the type to 1 which is
# the numerical equivalent of the PurplePrefType for choice.
$ppref = Purple::PluginPref->new_with_name_and_label(
"/plugins/core/perl_test/choice_str", "Choice Preference");
$ppref->set_type(1);
$ppref->add_choice("ch0", "ch0");
# The following will be the default value as set from plugin_load
$ppref->add_choice("ch1", "ch1");
$frame->add($ppref);
# Create a set of choices. To do so we must set the type to 1 which is
# the numerical equivalent of the PurplePrefType for choice.
$ppref = Purple::PluginPref->new_with_name_and_label(
"/plugins/core/perl_test/choice_int", "Choice Preference 2");
$ppref->set_type(1);
$ppref->add_choice("zero", 0);
# The following will be the default value as set from plugin_load
$ppref->add_choice("one", 1);
$frame->add($ppref);
# Create a text box. The default value will be "Foobar" as set by
# plugin_load
$ppref = Purple::PluginPref->new_with_name_and_label(
"/plugins/core/perl_test/text", "Text Box Preference");
$ppref->set_type(2);
$ppref->set_max_length(16);
$frame->add($ppref);
return $frame;
}

Request Dialog Box API

The Purple::Request package allows plugins to have interactive dialog boxes without the need to directly interact with the UI (e.g. GTK+). This allows core (libpurple) plugins to create a UI that can be displayed on whichever UI frontend is being used. The portion of the Request API available to perl scripts is listed below, followed by an example illustrating its use.

These arguments are the same for each of the three request types:

  • handle - The plugin handle.
  • title - String title for the dialog.
  • primary - The first sub-heading.
  • secondary - The second sub-heading.
  • ok_text - The Text for the OK button.
  • ok_cb - The string name of the perl subroutine to call when the OK button is clicked.
  • cancel_text - The text for the Cancel button.
  • cancel_cb - The string name of the perl subroutine to call when the Cancel button is clicked.
  • default_value - Default text string to display in the input box.
  • multiline - Boolean where true indicates multiple line input boxes are allowed.
  • masked - Boolean indicating if the user can edit the text.
  • hint - See source for more information - can be left blank.
  • filename - String default file name value.
  • savedialog - Boolean where true indicates use as a save file dialog and false indicates an open file dialog.
# Create a simple text input box
Purple::Request::input(handle, title, primary, secondary, default_value, multiline, masked, hint, ok_text, ok_cb, cancel_text, cancel_cb);
# Prompt user to select a file
Purple::Request::file(handle, title, filename, savedialog, ok_cb, cancel_cb);
# Create a unique input dialog as shown in the following example
Purple::Request::fields(handle, title, primary, secondary, fields, ok_text, ok_cb, cancel_text, cancel_cb);

The following is an example of a Purple::Request::fields() dialog box with a callback for the OK button and another for the Cancel Button.

sub ok_cb_test{
# The $fields is passed to the callback function when the button is clicked.
# To access a specific field, it must be extracted from $fields by name.
$fields = shift;
$account = Purple::Request::Fields::get_account($fields, "acct_test");
$int = Purple::Request::Fields::get_integer($fields, "int_test");
$choice = Purple::Request::Fields::get_choice($fields, "ch_test");
}
sub cancel_cb_test{
# Cancel does nothing but is equivalent to the ok_cb_test
}
sub plugin_load {
my $plugin = shift;
# Create a group to pool together multiple fields.
$group = Purple::Request::Field::Group::new("Group Name");
# Each field is created with Purple::Request::*_new(), is made viewable with Purple::Request::field_*_set_show_all()
# and is then added to the group with Purple::Request::field_group_add_field()
# Add an account combobox containing all active accounts
$field = Purple::Request::Field::account_new("acct_test", "Account Text", undef);
Purple::Request::Field::account_set_show_all($field, 0);
Purple::Request::Field::Group::add_field($group, $field);
# Add an integer input box
$field = Purple::Request::Field::int_new("int_test", "Integer Text", 33);
Purple::Request::Field::Group::add_field($group, $field);
# Add a list of choices
$field = Purple::Request::Field::choice_new("ch_test", "Choice Text", 1);
Purple::Request::Field::choice_add($field, "Choice 0");
Purple::Request::Field::choice_add($field, "Choice 1");
Purple::Request::Field::choice_add($field, "Choice 2");
Purple::Request::Field::Group::add_field($group, $field);
# Create a Purple::Request and add the group that was just created.
$request = Purple::Request::Fields::new();
Purple::Request::Fields::add_group($request, $group);
# Display the dialog box with the input fields added earlier with the appropriate titles.
Purple::Request::fields(
$plugin,
"Request Title!",
"Primary Title",
"Secondary Title",
$request,
"Ok Text", "ok_cb_test",
"Cancel Text", "cancel_cb_test");
}

Misc: Plugin Actions, Timeouts and Callbacks

This section of the manual covers some of the important features that can be added to libpurple perl Plugins. Plugin actions are callback functions that are accessible to the user from the Buddy List window under "Tools -> [Plugin Name]". Timeouts allow a plugin to execute a perl subroutine after a given period of time. Note that timeouts only occur once, so if the timeout must occur periodically, the timeout needs to be re-added at the end of the timeout callback function. Callbacks are functions that are called when an event occurs (such as a buddy signing-on, or a message being received). The following three examples will demonstrate the usage of these features.

Plugin actions require the %PLUGIN_INFO hash to have a key/value pair added, specifying a perl subroutine which will list the available actions; each action listed must have a definition, including a reference to a callback subroutine, added to the %plugin_actions hash.

%PLUGIN_INFO = {
...,
plugin_action_sub => "plugin_actions_cb",
}
sub plugin_actions_cb {
my @actions = ("Action 1", "Action 2");
}
%plugin_actions = (
# The callback subroutine that is called when "Tools -> Plugin -> Action 1" is selected
"Action 1" => \&action1_cb,
# The callback subroutine that is called when "Tools -> Plugin -> Action 2" is selected
"Action 2" => \&action2_cb
);
sub action1_cb {
Purple::Debug::info("Test Plugin", "Action 1 activated\n");
}
sub action2_cb {
Purple::Debug::info("Test Plugin", "Action 2 activated\n");
}

Timeouts allow a perl subroutine to be executed after a specified time. They only occur once, so, as stated earlier, the timeout must be re-registered after every time it is called.

sub timeout_cb {
my $plugin = shift;
Purple::Debug::info("testplugin", "Timeout occurred.\n");
# Reschedule timeout
Purple::timeout_add($plugin, 10, \&timeout_cb, $plugin);
}
sub plugin_load {
$plugin = shift;
# Schedule a timeout for ten seconds from now
Purple::timeout_add($plugin, 10, \&timeout_cb, $plugin);
}

Callbacks are handled by creating a perl subroutine to serve as the callback and then attaching the callback to a signal.

sub signal_cb {
# The signal data and the user data come in as arguments
my ($account, $data) = @_;
Purple::Debug::info("testplugin", "Account \"" . $account->get_username() . "\" just connected.\n");
}
sub plugin_load {
$plugin = shift;
# User data to be given as an argument to the callback perl subroutine.
$data = "";
# A pointer to the handle to which the signal belongs needed by the callback function
$accounts_handle = Purple::Accounts::get_handle();
# Connect the perl subroutine 'signal_cb' to the event 'account-connecting'
Purple::Signal::connect($accounts_handle, "account-connecting", $plugin, \&signal_cb, $data);
}

Resources

See Also
API Documentation
All information, including names and email addresses, entered onto this website or sent to mailing lists affiliated with this website will be public. Do not post confidential information, especially passwords!