Sophie

Sophie

distrib > Mandriva > 9.1 > ppc > by-pkgid > 139f1937008153f7be215a71bc01ff9c > files > 58

ayttm-0.2.3-1mdk.ppc.rpm

Everybuddy Service Module Writing Howto
by Philip S Tellis, philip dot tellis at gmx dot net
v1.0, a very hot 18th May 2002
v1.1, a sultry, and slightly rainy 3rd August 2002

$Revision: 1.1.1.1 $
$Date: 2003/04/01 07:24:13 $

I recently wrote the new Yahoo2 module for everybuddy, and well, to say the
least, the early parts were just one stumbling block after another.  There's
a lot I learnt after getting past that stage, so I'm going to put it down
here that others don't have the same problems.


0. About this document

0.1.  Copyright

Copyright C 2002 by Philip S Tellis.

Permission is granted to make and distribute copies of this manual
provided the copyright notice and this permission notice are preserved on
all copies.

Permission is granted to copy and distribute modified versions of this
manual under the conditions for verbatim copying, provided that the
derived work is distributed under the terms of a permission notice
identical to this one.  Translations fall under the catagory of "modified
versions".

This document is part of the everybuddy project (http://www.everybuddy.com/)

0.2.  Disclaimer

The information in this document is based on my own experience.  It worked
for me, and in all probability should work for you too, however, I make no
guarantees.  If your code segfaults after following these instructions, 
*you* have to debug it.

0.3.  Feedback

If you have questions or comments about this document, I'm on the everybuddy
user and dev mailing lists.

0.4.  Where to get this Howto

This howto should be distributed with the everybuddy project, in the docs
directory.


1. Introduction

Thanks to Alex Wheeler, Everybuddy now uses modules for services and utilities.
Anyone can write a module, provided they follow the API, and understand some
of the variable naming conventions and restrictions.

The interfaces for the service modules and utility modules are different, but
I've only looked at the service modules, so this document will be only about
writing service modules.

A service, is simply a particular IM protocol, so ICQ, AIM, MSN, Yahoo and
Jabber are all separate services.

1.1.  What I'll assume

I assume you know C, and know which standard functions are declared in which 
libraries.  If you can't do this, stop now, and go learn C.  You don't know 
what you're missing.

You should also have some knowledge of how to use gdb.  If you're going to
develop a module, you'll be using gdb a lot.

You may need to know a bit about glib/gtk, but we'll try and keep that to a
minimum, and I'll refer you to documentation when something comes up.  You
should check out the glib library anyway, because they have quite a few
good functions and types that will be used often.

1.2.  What you'll need

I'll assume that you have a CVS checkout of everybuddy, because we'll use 
autoconf/automake to generate our makefile based on everybuddy's defaults.

You'll also need libtool version 1.4 or later if you want useful error
messages in the module loader.  You can develop with earlier versions, but
if you do get errors, you'll be groping in the dark with the wrong message.

1.3.  I18n

Everybuddy uses gettext for internationalisation.  We'll deal with this in
section 11.


2. A skeleton module

We'll start off by writing a skeleton module, that doesn't do anything, but
can be compiled and loaded into everybuddy.

2.1.  Module directory

First, create a new directory under the modules directory of everybuddy's
source.  I'll call this directory test, you call it anything you want to.
We will write our module code and makefiles inside this directory.


2.2.  The module code

Start your C file by putting the following code into it.  I'll use a module
called test.  You should replace all occurances of test with the name of your 
service.

Your source file can have any name, what's important is what you use in the
four #define lines at the start of the module code section below.

/*
 * test.c
 */

#include "service.h"
#include "plugin_api.h"
#include "smileys.h"

/*******************************************************************************
 *                             Begin Module Code
 ******************************************************************************/
/*  Module defines */
#define plugin_info test_LTX_plugin_info
#define SERVICE_INFO test_LTX_SERVICE_INFO
#define plugin_init test_LTX_plugin_init
#define plugin_finish test_LTX_plugin_finish

/* Function Prototypes */
int plugin_init();
int plugin_finish();
struct service_callbacks * query_callbacks();

static int ref_count = 0;

/*  Module Exports */
PLUGIN_INFO plugin_info =
{
	PLUGIN_SERVICE,
	"Test Service",
	"Test Service Module",
	"$Revision: 1.1.1.1 $",
	"$Date: 2003/04/01 07:24:13 $",
	&ref_count,
	plugin_init,
	plugin_finish,
	NULL
};
struct service SERVICE_INFO = {
	"Test", 		// Name
	-1,			// Protocol id - leave as -1
	FALSE, 			// can do offline messaging
	FALSE, 			// can do conferencing
	FALSE, 			// can do native file transfer
	FALSE, 			// can do iconv
	NULL			// pointer to service callbacks - leave NULL
};
/* End Module Exports */

int plugin_init()
{
	ref_count = 0;
	plugin_info.prefs = NULL;

	return (0);
}

int plugin_finish()
{
	eb_debug(DBG_MOD, "Returning the ref_count: %i\n", ref_count);
	return (ref_count);
}

/*******************************************************************************
 *                             End Module Code
 ******************************************************************************/



And at the end of your code, add the service callbacks function.  The 
service_callbacks structure is a list of function pointers.  You assign
to each, the function in your module that performs the particular task.

For now, we'll keep all of them NULL.


struct service_callbacks *query_callbacks()
{
	struct service_callbacks *sc;

	sc = g_new0(struct service_callbacks, 1);

	sc->query_connected 		= NULL;
	sc->login			= NULL;
	sc->logout 			= NULL;
	sc->check_login			= NULL;

	sc->send_im 			= NULL;

	sc->read_local_account_config 	= NULL;
	sc->write_local_config 		= NULL;
	sc->read_account_config 	= NULL;

	sc->get_states 			= NULL;
	sc->get_current_state 		= NULL;
	sc->set_current_state 		= NULL;
	
	sc->new_account			= NULL;
	sc->add_user 			= NULL;
	sc->del_user 			= NULL;

	sc->is_suitable			= NULL;

	sc->get_status_string 		= NULL;
	sc->get_status_pixmap 		= NULL;

	sc->set_idle 			= NULL;
	sc->set_away 			= NULL;

	sc->send_chat_room_message 	= NULL;

	sc->join_chat_room 		= NULL;
	sc->leave_chat_room 		= NULL;

	sc->make_chat_room 		= NULL;

	sc->send_invite 		= NULL;
	sc->accept_invite 		= NULL;
	sc->decline_invite 		= NULL;

	sc->send_file			= NULL;

	sc->terminate_chat		= NULL;

	sc->get_info 			= NULL;
	sc->get_prefs 			= NULL;
	sc->read_prefs_config 		= NULL;
	sc->write_prefs_config 		= NULL;

	sc->add_importers 		= NULL;
	
	sc->get_color			= NULL;
	sc->get_smileys			= NULL;

	return sc;
}


Well, it wasn't really necessary to set all of them to null, since the
call to g_new0 does that, but if we're going to have to assign actual
function addresses to them at some point, it makes sense to keep all of
them ready right now.

Just make sure that this is always the last function in your file.  It
needs to be aware of all the functions that it refers to.

That should be sufficient to get your module compiled and loaded into
everybuddy.  There's just one more thing to do.  Create the Makefile
that will build and install the module correctly.

2.3.  The Makefile

Since everybuddy uses autoconf to build a source distribution, your job
of making the makefile is easy.

Create a file called Makefile.am in the test directory, with the following
contents:


SUBDIRS = 
libdir = $(datadir)/everybuddy/modules

CFLAGS = @CFLAGS@ -I$(top_srcdir)/src $(GTK_CFLAGS)

EXTRA_DIST = 

lib_LTLIBRARIES = test.la
test_la_SOURCES = test.c

test_la_LDFLAGS = -module 


Just make sure that the value you give for lib_LTLIBRARIES matches the 
next two variable names, and the name you gave to your service at the
start of your source (those four #define lines).  eg, for my yahoo2 module, 
the last two lines look like this:

lib_LTLIBRARIES = yahoo2.la
yahoo2_la_SOURCES = yahoo.c

yahoo2_la_LDFLAGS = -module 

To build the makefile, first cd to everybuddy's top level directory - the
one that has configure in it.  Now type the following at the shell prompt:

CONFIG_FILES=modules/test/Makefile CONFIG_HEADERS=  sh config.status

That should create a makefile in your module directory.  Change back to
your module directory.


2.4.  Compiling, loading and testing

You only need to run make and make install inside your module's directory
so that only that module will be compiled and installed.

If you already have everybuddy running, then go to the Preferences menu, 
and select the Modules tab.  Click on Rescan.  If all went well, you should
now see your module among the list of loaded modules.

If there was an error, you'll get an error message telling you what went wrong.
(Assuming of course that you have libtool-1.4 or later).

2.5.  The callbacks

At this point, you should probably go and look at service.h and have a look
at the signatures for the functions that you need to implement.  Not all of
them need to be implemented, the rest of this howto will list out what needs
to be implemented.


3. Run time configuration

Now that we have a loadable module, we need to be able to configure it at
run time.  There are many things that can be configured, we'll look at those
that are generally included in all modules.

3.1.  Creating the prefs list

To create the prefs list, we need to first include the input_list and
dialog apis. Add 

#include "dialog.h"
#include "input_list.h"

to the includes section (sometimes called the preamble) of your C file.
Although I think that MAX_PREF_LEN should probably be defined in a file
called prefs.h

Then, we create our input list in plugin_init. 

int plugin_init()
{
	input_list *il = g_new0(input_list, 1);

	ref_count = 0;

	plugin_info.prefs = il;
	il->widget.entry.value = service_host;
	il->widget.entry.name = service_host;
	il->widget.entry.label = "Server:";
	il->type = EB_INPUT_ENTRY;

	il->next = g_new0(input_list, 1);
	il = il->next;
	il->widget.entry.value = service_port;
	il->widget.entry.name = service_port;
	il->widget.entry.label = "Port:";
	il->type = EB_INPUT_ENTRY;

	il->next = g_new0(input_list, 1);
	il = il->next;
	il->widget.checkbox.value = &do_test_debug;
	il->widget.checkbox.name = do_test_debug;
	il->widget.checkbox.label = "Enable debugging";
	il->type = EB_INPUT_CHECKBOX;

	return 0;
}


3.2.  Variables to store our prefs

We also need to declare and define the variables that will hold our prefs.
These are the variables used above - service_host, service_port and
do_test_debug.

The first two are character arrays, and the third is an int or gboolean.

Declare them before you define plugin_init().  It's a good convention to
put them at the same place as the declaration of ref_count.  We can also
set default values for these prefs at this point.

static char service_host[MAX_PREF_LEN] = "testserver.com";
static char service_port[MAX_PREF_LEN] = "8080";
static int do_test_debug = 0;

We make them static so that they cannot be used outside the module, but if
your module runs into multiple source files that all need to access these
variables, then drop the static class.

Note that the size of all character arrays used as prefs MUST be at least
MAX_PREF_LEN.  Anything less than this will most certainly cause a segfault
when reading prefs from file.

That's it.  We now have a user configurable module.  Recompile and install
it by running make and make install.  If you already have everybuddy running,
then just go to the modules list, right click on your module and choose
reload.  Then see your prefs listed out there.


3.3.  Making use of do_test_debug

Since we let the user turn on debugging messages at run time, we should add 
code that will show or hide debugging info depending on the value of this
variable.

Again, we have Alex to thank for making this a run time directive rather than
compile time.

Add the following code just after your module section.

#ifdef __STDC__
int TEST_DEBUGLOG(char *fmt,...)
#else
int TEST_DEBUGLOG(fmt, va_alist)
char *fmt;
va_dcl
#endif
{
	va_list ap;

#ifdef __STDC__
	va_start(ap, fmt);
#else
	va_start(ap);
#endif

	vfprintf(stderr, fmt, ap);
	fflush(stderr);
	va_end(ap);
	return 0;
}


#define LOG(x) if(do_test_debug) { TEST_DEBUGLOG("%s:%d: ", __FILE__, __LINE__); \
	TEST_DEBUGLOG x; \
	TEST_DEBUGLOG("\n"); }

#define WARNING(x) if(do_test_debug) { TEST_DEBUGLOG("%s:%d: warning: ", __FILE__, __LINE__); \
	TEST_DEBUGLOG x; \
	TEST_DEBUGLOG("\n"); }



Try it out by adding a few LOG and WARNING messages in your code.

You must use a double pair of parentheses in your calls to LOG or WARNING.  
eg: LOG(("Error with %s", ea->handle));

If you understand how #define works, you'll know what happens here.


4. Account Configuration

Now that we have a service, everybuddy will allow users to add accounts with 
that service.  We need to be able to read this account information.

In fact, we must provide at least some definition, even if it does nothing, 
or we'll get a segfault when everybuddy tries to call these functions.

4.1.  Configuration stubs

Add the following two function stubs, and add them to the query_callbacks
function:

eb_local_account *read_local_account_config(GList * pairs)
{
	eb_local_account *ela = g_new0(eb_local_account, 1);

	ela->handle = strdup(value_pair_get_value(pairs, "SCREEN_NAME"));
	strncpy(ela->alias, ela->handle, sizeof(ela->alias));

	ela->service_id = SERVICE_INFO.protocol_id;
	ela->protocol_local_account_data = NULL;

	return ela;
}

GList * write_local_config(eb_local_account *account)
{
	GList *list = NULL;
	value_pair *vp;

	vp = g_new0(value_pair, 1);
	strcpy(vp->key, "SCREEN_NAME");
	strcpy(vp->value, account->handle);

	list = g_list_append(list, vp);

	vp = g_new0(value_pair, 1);
	strcpy(vp->key, "PASSWORD");
	strcpy(vp->value, "");

	list = g_list_append(list, vp);

	return list;
}

It should be evident from the names where they need to go in the callbacks
code.

You'll also have to include value_pair.h to handle the value_pair type.

So... anything missing?  If you've been paying attention, then you'll have
noticed that we ignored the password in read config and just wrote back a
blank password in write config.

This is so because eb_local_account doesn't store passwords.  That would be
protocol specific information, so would be handled by the protocol local
account data.  You'll also have noticed that we set this to NULL.

4.2.  protocol_local_account_data

Every service would have certain unique fields.  The eb_local_account
structure only holds information that is of importance to everybuddy, ie
that which is required to display on screen, and identify the module that
will be used to do the dirty work.  Anything else is stored in 
protocol_local_account_data.  

protocol_local_account_data is of type void*, so we can cast it to any type of
our choice.  We could just make it a character array to store the password, 
but we'll think of the future, and make it a struct instead that we can
extend when we need to.

Somewhere at the top of your file (after the module section), add your struct
definition.  For convenience, we'll also typedef it.

typedef struct eb_test_local_account_data {
	char password[255];
} eb_test_local_account_data;

Now, in eb_read_local_account_config, add code to use the protocol local
account data:

	eb_test_local_account_data *tla = g_new0(eb_test_local_account_data, 1);
	
	...
	strncpy(tla->password, value_pair_get_value(pairs, "PASSWORD"),
			sizeof(tla->password));
	...
	
	ela->protocol_local_account_data = tla;

and also in eb_write_local_config:

	eb_test_local_account_data *tla = account->protocol_local_account_data;

	...
	strcpy(vp->value, tla->password);


That should do it.  Compile, install, reload, and test it by creating an
account.  Hmmm... segfault.  Use gdb to figure out where it occurs.  We'll
have to implement those methods next.


5. The status menu

When an account exists, everybuddy queries the account's service module for
a status menu.  This status menu can be used to change the state of the
account.  We need to provide everybuddy with a GList of all possible states.

Order is important here, because when everybuddy needs to set the state of an
account, it will send back the index of the state in this list, and when it
queries the module for the current state, we need to return an index.

5.1.  Returning all our states

Create a function returning a list of all states:

GList * eb_test_get_states()
{
	GList * states = NULL;

	states = g_list_append(states, "Online");
	states = g_list_append(states, "Offline");
	states = g_list_append(states, "Away");

	return states;
}

Then register it as a callback.

	sc->get_states = eb_test_get_states;

You can now look at the glib documentation for GList.  You don't need to, but
it's a good idea.  GList is an implementation of a doubly linked list.


5.2.  The current state

We now need to tell everybuddy what our current state is.  Of course, we first
need to have some place to store our current state.  Go back to the
eb_test_local_account_data struct that we created, and add a field for the
current status:

typedef struct eb_test_local_account_data {
	char password[255];
	int status;
} eb_test_local_account_data;

and initialise it in read_local_account_config to the offline value.

Then, create eb_test_get_current_state:

gint eb_test_get_current_state(eb_local_account *account)
{
	eb_test_local_account_data *tla = account->protocol_local_account_data;

	return tla->status;
}

Of course, this assumes that your service has status numbers that run
sequentially from 0 up in steps of 1.  If it doesn't, then you'll have to add
some conversion mechanism.  Just remember that everybuddy expects an integer
from 0 up.

Register this function as a callback.

5.3.  Setting the state

Finally, give everybuddy a mechanism to set the state.

void eb_test_set_current_state(eb_local_account *account, gint state)
{
	eb_test_local_account_data *tla = account->protocol_local_account_data;
	
	tla->status = state;
}

Of course, you'll want it to do a lot more, like actually changing the state
on the server, going online and offline, etc.

5.4.  Readable states

It's not very easy to read your code when it contains a whole load of numbers
like this, so many of use use enums instead:

enum test_states {
	ONLINE = 0,
	OFFLINE,
	AWAY
};

and use those values instead.


6. Buddies

We now need to add code to add and remove buddies.  If these callbacks don't
exist, everybuddy will segfault.  The first thing we need, is some way to keep
track of the status of all our buddies.

6.1.  Buddy status

Define a new structure called eb_test_account_data:

typedef struct eb_test_account_data {
	gint status;
} eb_test_account_data;


6.2.  Adding a new contact

Create a function that given an account handle, returns a new eb_account for
your service.

eb_account * eb_test_new_account(gchar * account)
{
	eb_account *ea = g_new0(eb_account, 1);
	eb_test_account_data *tad = g_new0(eb_test_account_data, 1);

	ea->protocol_account_data = tad;

	strncpy(ea->handle, account, 255);
	ea->service_id = SERVICE_INFO.protocol_id;
	tad->status = TEST_STATE_OFFLINE;

	return ea;
}


6.3.  Adding and removing buddies on the server

You should also write functions to add and remove your buddies from the
server.  These functions must exist for everybuddy to run.

void eb_test_add_user(eb_account * account)
{

}

void eb_test_del_user(eb_account * account)
{

}

And add them as callbacks.

This will allow you to add contacts for the service.

6.4.  Reading contacts from your contact file

Also add code to read your account from the contacts file:

eb_account *eb_test_read_account_config(GList * config, struct contact * contact)
{
	eb_account *ea = g_new0(eb_account, 1);
	eb_test_account_data *tad = g_new0(eb_test_account_data, 1);

	tad->status = TEST_STATE_OFFLINE;

	strncpy(ea->handle, value_pair_get_value(config, "NAME"),
			sizeof(ea->handle));

	ea->service_id = SERVICE_INFO.protocol_id;
	ea->protocol_account_data = tad;
	ea->account_contact = contact;
	ea->icon_handler = -1;
	ea->status_handler = -1;

	eb_test_add_user(ea);

	return ea;
}


7. Buddy status

For a buddy to be displayed in the buddy list, everybuddy requires a pixmap to
display against the buddy name, a status string, and must be able to query
whether a buddy is online or not.

7.1.  Querying a buddy's status

gboolean eb_test_query_connected(eb_account * account)
{
	eb_test_account_data *tad = account->protocol_account_data;

	if (ref_count <= 0) {
		tad->status = TEST_STATE_OFFLINE;
	}
	return tad->status != TEST_STATE_OFFLINE;
}

7.2.  Returning a status string

You need to implement a function that given an eb_account, returns the status
string associated with that.  This could simply be something like Online,
Offline, Away.

gchar *eb_test_get_status_string(eb_account * account)
{
	return "";
}


7.3.  Returning a status pixmap

Everybuddy uses standard pixmaps with different colours for each service.  You
will have to modify some of the existing pixmaps to make your own.

#include "pixmaps/test_online.xpm"
#include "pixmaps/test_away.xpm"

static gint pixmaps = 0;
static GdkPixmap *eb_test_pixmap[2];
static GdkBitmap *eb_test_bitmap[2];

void eb_test_init_pixmaps()
{
	eb_test_pixmap[0] = gdk_pixmap_create_from_xpm_d(statuswindow->window,
			&eb_test_bitmap[0], NULL, test_online_xpm);

	eb_test_pixmap[1] = gdk_pixmap_create_from_xpm_d(statuswindow->window,
			&eb_test_bitmap[1], NULL, test_away_xpm);

	pixmaps = 1;
}

void eb_test_get_status_pixmap(eb_account * account, GdkPixmap ** pm, GdkBitmap ** bm)
{
	eb_test_account_data *tad;

	if (!pixmaps)
		eb_test_init_pixmaps();

	tad = account->protocol_account_data;

	if(tad->status == TEST_STATE_ONLINE) {
		*pm = eb_test_pixmap[0];
		*bm = eb_test_bitmap[0];
	} else {
		*pm = eb_test_pixmap[1];
		*bm = eb_test_bitmap[1];
	}

}

You'll also have to include util.h to get access to the statuswindow.

8. Logging in and out

You must also implement the login and logout functions that are called when
one selects Sign on all and Sign off all from the menu.

8.1.  Login

Your login code would perform the actual login process, this is just skeleton
code.

void eb_test_login(eb_local_account *account)
{
	eb_test_local_account_data *tla = account->protocol_local_account_data;

	tla->status = TEST_STATE_ONLINE;
	eb_test_set_current_state(account, TEST_STATE_ONLINE);
	ref_count++;
	account->connected = 1;
}

8.2.  Logout

Your logout code would perform the actual logout process.  You should also
call buddy_logoff for all your buddies after you have completed logging
off.

void eb_test_logout(eb_local_account *account)
{
	eb_test_local_account_data *tla = account->protocol_local_account_data;

	tla->status = TEST_STATE_OFFLINE;
	ref_count--;
	account->connected = 0;
	eb_test_set_current_state(account, TEST_STATE_OFFLINE);

	// for(buddies) {
	//	buddy_logoff(buddy);
	//	buddy_update_status(buddy);
	// }
}

8.3.  Updating the status menu on logon/logoff

It would be nice if the status menu actually reflected your logged in/out
status.  To do this, you need to call eb_set_active_menu_status:

	if(account->status_menu)
		eb_set_active_menu_status(account->status_menu, TEST_STATE_ONLINE);

and similarly for the offline state.

Be careful though, because eb_set_active_menu_status calls your
set_current_state function.  If this function calls your login/logout
functions to login/logout, then you could end up with a loop.

To avoid this, we use a global variable is_setting_state.  This is a dirty
hack, but it works for now, so until someone has a better idea, this is what
we'll do.

At the start of the file:

static int is_setting_state = 0;

In eb_test_set_current_state, right at the top of the function:

	if(is_setting_state)
		return;

And for every call to eb_set_active_menu_status:

	if(account->status_menu) {
		is_setting_state = 1;
		eb_set_active_menu_status(account->status_menu, TEST_STATE_ONLINE);
		is_setting_state = 0;
	}

9. Smileys and Service Colour

As of version 0.4.2, everybuddy supports pixmap smileys.  Each module is
responsible for providing their own smiley list, but everybuddy provides
a default list for those that don't want to.  Thanks to Meredyyd for this.

9.1.  Setting your smileys

For now we'll just use the default, and you can read through that code to
figure out how to do your own.

In query_callbacks, attach an entry for get_smileys:

        sc->get_smileys                 = eb_default_smileys;
	
This will tell everybuddy to use the default smiley list when needed.


9.2.  Service colour

When in a chat window, it's hard to tell at a glance which service you're
using to chat with someone.  This is especially a problem when the service
you're using goes down and everybuddy falls back to an alternate service.

Instead, everybuddy now uses a service colour to identify each service in
the chat window.  Your buddy's handle will show up in the colour that you
specify here.

First, add your colour function:

	#define TEST_MSG_COLOR   "#808080"
	static char *eb_test_get_color(void) { return TEST_MSG_COLOR; }

This colour would ideally be the same as the pixmap that you use for your
buddies.  It can be a static string since everybuddy will not free it.

Next, add the callback in query_callbacks.


10. Finishing up

Well, that's all you need to implement a service module that can be loaded
into everybuddy.  You still have to implement sending of messages and setting 
of idle and away states, but I think you can manage that from here.


11. I18n

Thanks to Colin Leroy, everybuddy has internationalisation support.  If you
don't know what that means, you've got to do some reading on the web.


11.1. intl.h

To enable your module for i18n support, you've got to do a few things.  First,
add the i18n API to your module:

	#include "intl.h"

Ideally, this should be the first of your includes.


11.2. Replacing the strings

Next, locate all the user displayed text strings in your code.  For the test 
module, this would be our three prefs: Server, Port and Enable Debugging.

All we as programmers need to do is enclose these strings in _() function
calls:

	...
	il->widget.entry.label = _("Server:");
	...
	il->widget.entry.label = _("Port:");
	...
	il->widget.checkbox.label = _("Enable debugging");
	...

If your module has any other strings, do it for them too.  Don't do it for
debugging messages - those that are written via LOG or WARNING.

11.3. Making the translation template

You'll also have to do a bit to get a translation template file that can be
used by the translators.

First, replace every _() with gettext().
Then run xgettext test.c
That will give you a messages.po file.  You should concatenate this to the
existing .po files in everybuddy's po directory.
Then, replace all the gettext() back to _().


12. Acknowledgements

* Thanks to Torrey Searle for creating everybuddy
* Thanks to Alex Wheeler, without whom there would be no modules
* All the members of the Everybuddy-dev team, who helped a lot when I was
  writing the yahoo2 module
* Thanks to Yahoo for forcing me to figure out how to write modules
* Thanks to Meredydd for smiley support
* Thanks to Colin for i18n support and lots more


13. Further Reading

Well, read the sources of the other modules, you'll learn a lot, and read the
headers that are included in your file.

* The Glib and GTK documentation


Appendix A: test.c


/*
 * test.c
 */

#include <string.h>
#include "service.h"
#include "plugin_api.h"
#include "dialog.h"
#include "input_list.h"
#include "value_pair.h"
#include "util.h"

/*******************************************************************************
 *                             Begin Module Code
 ******************************************************************************/
/*  Module defines */
#define plugin_info test_LTX_plugin_info
#define SERVICE_INFO test_LTX_SERVICE_INFO
#define plugin_init test_LTX_plugin_init
#define plugin_finish test_LTX_plugin_finish

/* Function Prototypes */
int plugin_init();
int plugin_finish();
struct service_callbacks * query_callbacks();

static int is_setting_state = 0;

static int ref_count = 0;

char service_host[MAX_PREF_LEN] = "testserver.com";
char service_port[MAX_PREF_LEN] = "80";
static int do_test_debug = 0;

/*  Module Exports */
PLUGIN_INFO plugin_info =
{
	PLUGIN_SERVICE,
	"Test Service",
	"Test Service Module",
	"$Revision: 1.1.1.1 $",
	"$Date: 2003/04/01 07:24:13 $",
	&ref_count,
	plugin_init,
	plugin_finish,
	NULL
};
struct service SERVICE_INFO = {
	"Test", 		// Name
	-1,			// Protocol id - leave as -1
	FALSE, 			// can do offline messaging
	FALSE, 			// can do conferencing
	FALSE, 			// can do native file transfer
	TRUE, 			// can do iconv
	NULL			// pointer to service callbacks - leave NULL
};
/* End Module Exports */

int plugin_init()
{
	input_list *il = g_new0(input_list, 1);
	ref_count = 0;

	plugin_info.prefs = il;
	il->widget.entry.value = service_host;
	il->widget.entry.name = service_host;
	il->widget.entry.label = _("Server:");
	il->type = EB_INPUT_ENTRY;

	il->next = g_new0(input_list, 1);
	il = il->next;
	il->widget.entry.value = service_port;
	il->widget.entry.name = service_port;
	il->widget.entry.label = _("Port:");
	il->type = EB_INPUT_ENTRY;

	il->next = g_new0(input_list, 1);
	il = il->next;
	il->widget.checkbox.value = &do_test_debug;
	il->widget.checkbox.name = do_test_debug;
	il->widget.checkbox.label = _("Enable debugging");
	il->type = EB_INPUT_CHECKBOX;


	return (0);
}

int plugin_finish()
{
	eb_debug(DBG_MOD, "Returning the ref_count: %i\n", ref_count);
	return (ref_count);
}

/*******************************************************************************
 *                             End Module Code
 ******************************************************************************/


#ifdef __STDC__
int TEST_DEBUGLOG(char *fmt,...)
#else
int TEST_DEBUGLOG(fmt, va_alist)
char *fmt;
va_dcl
#endif
{
	va_list ap;

#ifdef __STDC__
	va_start(ap, fmt);
#else
	va_start(ap);
#endif

	vfprintf(stderr, fmt, ap);
	fflush(stderr);
	va_end(ap);
	return 0;
}


#define LOG(x) if(do_test_debug) { TEST_DEBUGLOG("%s:%d: ", __FILE__, __LINE__); \
	TEST_DEBUGLOG x; \
	TEST_DEBUGLOG("\n"); }

#define WARNING(x) if(do_test_debug) { TEST_DEBUGLOG("%s:%d: warning: ", __FILE__, __LINE__); \
	TEST_DEBUGLOG x; \
	TEST_DEBUGLOG("\n"); }


enum eb_test_states {
	TEST_STATE_ONLINE=0,
	TEST_STATE_OFFLINE,
	TEST_STATE_AWAY
};
	
typedef struct eb_test_account_data {
	gint status;
} eb_test_account_data;

typedef struct eb_test_local_account_data {
	char password[255];
	int status;
} eb_test_local_account_data;

eb_local_account *read_local_account_config(GList * pairs)
{
	eb_local_account *ela = g_new0(eb_local_account, 1);
	eb_test_local_account_data *tla = g_new0(eb_test_local_account_data, 1);

	ela->handle = g_strdup(value_pair_get_value(pairs, "SCREEN_NAME"));
	strncpy(ela->alias, ela->handle, 255);
	strncpy(tla->password, value_pair_get_value(pairs, "PASSWORD"), 255);

	tla->status = TEST_STATE_OFFLINE;

	ela->service_id = SERVICE_INFO.protocol_id;
	ela->protocol_local_account_data = tla;

	return ela;
}

GList *write_local_config(eb_local_account * account)
{
	eb_test_local_account_data *tla = account->protocol_local_account_data;
	GList *list = NULL;
	value_pair *vp;

	vp = g_new0(value_pair, 1);
	strcpy(vp->key, "SCREEN_NAME");
	strcpy(vp->value, account->handle);

	list = g_list_append(list, vp);

	vp = g_new0(value_pair, 1);
	strcpy(vp->key, "PASSWORD");
	strcpy(vp->value, tla->password);

	list = g_list_append(list, vp);

	return list;
}

GList *eb_test_get_states()
{
	GList *states = NULL;

	states = g_list_append(states, "Online");
	states = g_list_append(states, "Offline");
	states = g_list_append(states, "Away");

	return states;
}

gint eb_test_get_current_state(eb_local_account *account)
{
	eb_test_local_account_data *tla = account->protocol_local_account_data;

	return tla->status;
}

void eb_test_set_current_state(eb_local_account *account, gint state)
{
	eb_test_local_account_data *tla = account->protocol_local_account_data;

	if(is_setting_state)
		return;

	tla->status = state;
}

eb_account *eb_test_new_account(gchar * account)
{
	eb_account *ea = g_new0(eb_account, 1);
	eb_test_account_data *tad = g_new0(eb_test_account_data, 1);

	ea->protocol_account_data = tad;

	strncpy(ea->handle, account, 255);
	ea->service_id = SERVICE_INFO.protocol_id;
	tad->status = TEST_STATE_OFFLINE;

	return ea;
}

void eb_test_add_user(eb_account * account)
{

}

void eb_test_del_user(eb_account * account)
{
}

eb_account *eb_test_read_account_config(GList * config, struct contact * contact)
{
	eb_account *ea = g_new0(eb_account, 1);
	eb_test_account_data *tad = g_new0(eb_test_account_data, 1);

	tad->status = TEST_STATE_OFFLINE;

	strncpy(ea->handle, value_pair_get_value(config, "NAME"), 255);

	ea->service_id = SERVICE_INFO.protocol_id;
	ea->protocol_account_data = tad;
	ea->account_contact = contact;
	ea->icon_handler = -1;
	ea->status_handler = -1;

	eb_test_add_user(ea);

	return ea;
}

gboolean eb_test_query_connected(eb_account * account)
{
	eb_test_account_data *tad = account->protocol_account_data;

	if (ref_count <= 0) {
		tad->status = TEST_STATE_OFFLINE;
	}
	return tad->status != TEST_STATE_OFFLINE;
}

gchar *eb_test_get_status_string(eb_account * account)
{
	return "";
}


#include "pixmaps/nomodule_online.xpm"
#include "pixmaps/nomodule_away.xpm"

static gint pixmaps = 0;
static GdkPixmap *eb_test_pixmap[2];
static GdkBitmap *eb_test_bitmap[2];

void eb_test_init_pixmaps()
{
	eb_test_pixmap[0] = gdk_pixmap_create_from_xpm_d(statuswindow->window,
			&eb_test_bitmap[0], NULL, nomodule_online_xpm);

	eb_test_pixmap[1] = gdk_pixmap_create_from_xpm_d(statuswindow->window,
			&eb_test_bitmap[1], NULL, nomodule_away_xpm);

	pixmaps = 1;
}

void eb_test_get_status_pixmap(eb_account * account, GdkPixmap ** pm, GdkBitmap ** bm)
{
	eb_test_account_data *tad;

	if (!pixmaps)
		eb_test_init_pixmaps();

	tad = account->protocol_account_data;

	if(tad->status == TEST_STATE_ONLINE) {
		*pm = eb_test_pixmap[0];
		*bm = eb_test_bitmap[0];
	} else {
		*pm = eb_test_pixmap[1];
		*bm = eb_test_bitmap[1];
	}

}

void eb_test_login(eb_local_account *account)
{
	eb_test_local_account_data *tla = account->protocol_local_account_data;

	if(account->status_menu) {
		tla->status = TEST_STATE_ONLINE;
		is_setting_state = 1;
		eb_set_active_menu_status(account->status_menu, TEST_STATE_ONLINE);
		is_setting_state = 0;
	} else
		eb_test_set_current_state(account, TEST_STATE_ONLINE);
	account->connected = 1;
	ref_count++;
}

void eb_test_logout(eb_local_account *account)
{
	eb_test_local_account_data *tla = account->protocol_local_account_data;

	account->connected = 0;
	ref_count--;
	if(account->status_menu) {
		tla->status = TEST_STATE_OFFLINE;
		is_setting_state = 1;
		eb_set_active_menu_status(account->status_menu, TEST_STATE_OFFLINE);
		is_setting_state = 0;
	} else
		eb_test_set_current_state(account, TEST_STATE_OFFLINE);
}

#define TEST_MSG_COLOR   "#808080"
static char *eb_test_get_color(void) { return TEST_MSG_COLOR; }

struct service_callbacks *query_callbacks()
{
	struct service_callbacks *sc;

	sc = g_new0(struct service_callbacks, 1);

	sc->query_connected 		= eb_test_query_connected;
	sc->login			= eb_test_login;
	sc->logout 			= eb_test_logout;
	sc->check_login			= NULL;

	sc->send_im 			= NULL;

	sc->read_local_account_config 	= read_local_account_config;
	sc->write_local_config 		= write_local_config;
	sc->read_account_config 	= eb_test_read_account_config;

	sc->get_states 			= eb_test_get_states;
	sc->get_current_state 		= eb_test_get_current_state;
	sc->set_current_state 		= eb_test_set_current_state;
	
	sc->new_account 		= eb_test_new_account;
	sc->add_user 			= eb_test_add_user;
	sc->del_user 			= eb_test_del_user;

	sc->is_suitable			= NULL;

	sc->get_status_string 		= eb_test_get_status_string;
	sc->get_status_pixmap 		= eb_test_get_status_pixmap;

	sc->set_idle 			= NULL;
	sc->set_away 			= NULL;

	sc->send_chat_room_message 	= NULL;

	sc->join_chat_room 		= NULL;
	sc->leave_chat_room 		= NULL;

	sc->make_chat_room 		= NULL;

	sc->send_invite 		= NULL;
	sc->accept_invite 		= NULL;
	sc->decline_invite 		= NULL;

	sc->send_file			= NULL;

	sc->terminate_chat		= NULL;

	sc->get_info 			= NULL;
	sc->get_prefs 			= NULL;
	sc->read_prefs_config 		= NULL;
	sc->write_prefs_config 		= NULL;

	sc->add_importers 		= NULL;

	sc->get_color 			= eb_test_get_color;
	sc->get_smileys 		= eb_default_smileys;

	return sc;
}


ChangeLog:

2002-08-03 Philip Tellis
	* Added Torrey to the Acks
	* Various spelling, grammar changes (not necessarily fixes)
	* Added entries for check_login, get_color and get_smileys 
	  to service_callbacks
	* Updates for new prefs format
	* Use sizeof() instead of absolute values
	* Add entry for smileys and service colour
	* Added entry for I18n (section 11)

2002-05-18 Philip Tellis
	* First release