=========================================== Mcabber module writing brief howto =========================================== To obtain information on module mcabber uses struct module_info_t, that module should provide in public variable with name info_. If the module name contains any extra symbols except [a-z0-9_] they should be replaced with '_'. ------------------------------------------------------------------------ #include typedef void (*module_init_t)(void); typedef void (*module_uninit_t)(void); typedef struct module_info_struct module_info_t; struct module_info_struct { const gchar *branch; guint api; const gchar *version; const gchar *description; const gchar **requires; module_init_t init; module_uninit_t uninit; module_info_t *next; }; ------------------------------------------------------------------------ Callbacks init and uninit will be called after module and its dependencies loading. 'requires' can contain a NULL-terminated list of module names, that should be loaded before this. 'branch' and 'api' are required and should contain mcabber branch and api version, that this module is designed to work with. For these values see ChangeLog.api. 'version' and 'description' fields are optional and just provide user with additional information about the module. 'description' field can contain newlines. The 'next' field can contain pointer to the next struct with another branch of mcabber, if your module can work with multiple branches. To load modules, mcabber uses glib's GModule, thus, in your module you can also use functions ------------------------------------------------------------------------ #include #include const gchar* g_module_check_init (GModule *module); void g_module_unload (GModule *module); ------------------------------------------------------------------------ to do something before any version/dependency check is performed when your module is loaded/unloaded. On success g_module_check_init should return NULL, and error message otherwise. As module is loaded, you can use mcabber functions, declared in mcabber's header files (though you should consider, that they may change their calling conventions some day). I will not explain them all, there are too much of them, but will provide description for those, provided especially for module writers. ------------------------------------------------------------------------ #include const gchar *module_load (const gchar *name, gboolean manual, gboolean force); const gchar *module_unload (const gchar *name, gboolean manual, gboolean force); ------------------------------------------------------------------------ These functions load and unload modules respectively. You can use them to handle optional dependencies. What happens, when module is loaded: - check if module is present, and if present just increase it's reference count - load .so via glib (and call g_module_check_init, if present) - check for information structure presence - find suitable branch and check api version compatibility - load modules, that this module requires (note, that dependency problems will be reported as error invariably, force flag have no effect on this check) - module placed into a list of modules - module init routine is called And when unloaded: - check if module is present - decrease reference count, if it is not zero, return - run module uninit routine - unload modules, that were loaded as dependencies for this one - remove from modules list They return error message or NULL in case of success. 'manual' flag indicates, that module will be loaded by direct user request. It serves the purpose of tracking user and automatic references (user can have only one). 'force' flag on module loading causes mcabber to ignore most of the loading errors. On unload it forces unloading even if reference count is not zero. ------------------------------------------------------------------------ #include void cmd_add (const char *name, const char *help, guint flags1, guint flags2, void (*f)(char*), gpointer userdata); void cmd_del (const char *name); ------------------------------------------------------------------------ These two functions are provided to declare mcabber commands, offered by your module. - name is a command name. - help is a short description of your command, however for now it is not used at all and can be omitted. - flags are completion identifiers for first and second command arguments, for list of built-in completions, see compl.h. You can declare your own completion lists, using functions from compl.h, described later. - f is a user-provided callback function, that will be called upon executing mcabber command. If you will provide non-NULL userdata, function must be of type void (*f) (char *commandline, gpointer userdata). - userdata is a pointer to data, transparently passed to callback. See f description. ------------------------------------------------------------------------ #include guint compl_new_category (void); void compl_del_category (guint id); void compl_add_category_word (guint categ, const char *command); void compl_del_category_word (guint categ, const char *word); GSList *compl_get_category_list (guint cat_flags, guint *dynlist); ------------------------------------------------------------------------ These functions allow you to define and manage word lists for completion categories, used by your commands. First you need to obtain handle for completion type, that you later will supply as flags, when declaring your commands. For that use function compl_new_category. It returns new category id, or zero if mcabber runs out of completion ids (for now there are only 32 ids available, and 20 of them are already used for builtin commands). compl_del_category allows you to delete user-defined category, deleting all words in it too. Now, that you have a completion category, you can at any time add or delete words from its completion list. To do that, use the functions compl_add_category_word and compl_del_category_word. You can obtain current contents of category by using gompl_get_category_list. If after execution dynlist is TRUE, you should free obtained list of words (both, words and list). ------------------------------------------------------------------------ #include typedef struct { const char *name; const char *value; } hk_arg_t; typedef guint (*hk_handler_t) (const gchar *hookname, hk_arg_t *args, gpointer userdata); guint hk_add_handler (hk_handler_t handler, const gchar *hookname, gint priority, gpointer userdata); void hk_del_handler (const gchar *hookname, guint hid); ------------------------------------------------------------------------ These functions allow your module to react to events, such as incoming and outgoing messages, buddy status changes and server connection establishment or breakup. The hookname string specifies the events the handler wants to subscribe to. The available strings can be found in hooks.h. The hk_add_handler() function will return a handler id which you will use to remove the handler with hk_del_handler(). Args argument is a list of hk_arg_t structures, terminated by structure, whose name field is set to NULL. Your handler should return one of the values in the hk_handler_result enum (see hooks.h), usually HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS so that other handlers can be triggers as well. A handler can determine which event has occured by checking the hookname argument (a same hook handler can subscribe to several events by using hk_add_handler() several times). Currently the following events exist: - hook-pre-message-in (HOOK_PRE_MESSAGE_IN) with parameters * jid - sender of the incoming message * resource - resource of the incoming message * message - message body, converted to locale charset * groupchat - ("true" or "false") * delayed - message timestamp (ISO-8601 string) or empty string if the message wasn't delayed * error - "true" if this is an error message * carbon - "true" if this is a message carbon (cf. XEP-0280) - hook-post-message-in (HOOK_POST_MESSAGE_IN) with parameters * jid - sender of the incoming message * resource - resource of the incoming message * message - message body, converted to locale charset * groupchat - ("true" or "false") * attention - In a MUC message, true if you've been highlighted In a regular message, true if the sender has requested your attention (only implemented for MUC currently) * delayed - message timestamp (ISO-8601 string) or empty string if the message wasn't delayed * error - "true" if this is an error message * carbon - "true" if this is a message carbon (cf. XEP-0280) - hook-message-out (HOOK_MESSAGE_OUT) with parameters * jid - recipient of the outgoing message * message - message body, converted to locale charset - hook-mdr-received (HOOK_MDR_RECEIVED) with parameter * jid - recipient of the outgoing message - hook-status-change (HOOK_STATUS_CHANGE) with parameters * jid - buddy, whose status has changed * resource - resource, whose status has changed * old_status - old status of the buddy, one-char string, representing mcabber status letter - one of 'ofdna?_'. * new_status - new buddy status. Same as above. * message - new status message. Old one should still be available to module as the current buddy's message. - hook-my-status-change (HOOK_MY_STATUS_CHANGE) with parameters * new_status - user's new status, see hook-status-change. Old one should still be available as the current status of the user. * message - new status message - hook-post-connect (HOOK_POST_CONNECT) with no parameters - hook-pre-disconnect (HOOK_PRE_DISCONNECT) with no parameters - hook-unread-list-change (HOOK_UNREAD_LIST_CHANGE) * unread - number of buffers with the pending message flag (#) * attention - number of non-MUC buffers with the attention sign (!) * muc_unread - number of MUC buffers with the unread message flag * muc_attention - number of MUC buffers with the attention sign - hook-subscription (HOOK_SUBSCRIPTION) * type - the type of the subscription message received. Can be one of subscribe, unsubscribe, subscribed, unsubscribed. * jid - sender of the incoming subscription message * message - optional message sent with the request ------------------------------------------------------------------------ #include void xmpp_add_feature (const char *xmlns); void xmpp_del_feature (const char *xmlns); ------------------------------------------------------------------------ These functions may be useful, if your module implements some additional functionality to mcabber, that should be advertised in a client's discovery features list. ===================== Example: hello ===================== Now, let's write a simple module, called "hello", that will do no more than just print something on loading and unloading. ------------------------------------------------------------------------ #include #include /* We will use scr_log_print() mcabber function, that does mcabber's messages output */ #include /* Print something on module loading */ const gchar* g_module_check_init(GModule *module) { scr_log_print(LPRINT_NORMAL, "Hello, World!"); return NULL; } /* ... and unloading */ void g_module_unload(GModule *module) { scr_log_print(LPRINT_NORMAL, "Bye, World!"); } /* The End */ ------------------------------------------------------------------------ Now, compile this file (hello.c) with libtool --mode=compile gcc `pkg-config --cflags glib-2.0 \ gmodule-2.0 mcabber` -c hello.c libtool --mode=link gcc -module -rpath /usr/lib/mcabber/ \ `pkg-config --libs glib-2.0 gmodule-2.0 mcabber` \ -o libhello.la hello.lo (you should substitute /usr/lib/mcabber to the directory where your modules are located) and then install obtained module with libtool --mode=install install libhello.la \ /usr/lib/mcabber/libhello.la Note that you most likely need not run suggested by libtool finish action, as we're working with module object, not system- wide library, but maybe some systems require that. Now, set modules_dir mcabber variable to point to your modules directory, and try to run /module -f load hello. If all goes well, you should see in status buffer message "Hello World!" (as well as some complaints, as we forced module loading). Now unload module by running command /module unload hello, that should bring up message "Bye, World!". That's it, you just created very simple dynamically loadable mcabber module. But, as you noticed, it needs force to be loaded. Now, let's add the information structure that mcabber wants. ========================== Example: info struct ========================== ------------------------------------------------------------------------ #include /* module_info_t definition */ #include /* Print something on module loading */ void hello_init(void) { scr_log_print(LPRINT_NORMAL, "Hello, World!"); } /* ... and unloading */ void hello_uninit(void) { scr_log_print(LPRINT_NORMAL, "Bye, World!"); } module_info_t info_hello = { .branch = "dev", .api = 1, .version = "0.0.1", .description = "Hello world module\n" " (as well as bye world module)", .requires = NULL, .init = hello_init, .uninit = hello_uninit, .next = NULL, }; /* The End */ ------------------------------------------------------------------------ Here we still do not use glib nor gmodule, so, we can omit them in compilation lines: libtool --mode=compile gcc `pkg-config --cflags mcabber` \ -c hello.c libtool --mode=link gcc -module -rpath /usr/lib/mcabber/ \ `pkg-config --libs mcabber` -o libhello.la hello.lo Again compile it, copy, and try to load, now without -f flag. As you may notice, when loading previous example, mcabber first printed "Hello, World!", and only then complaint about module not having information struct. That's because g_module_check_init is called right after module loading, before mcabber even has a chance to look at module, while .init from info struct is called afterwards by mcabber itself. You can try to introduce some error (e.g. too high or missing target mcabber version) and see the difference. ======================= Example: command ======================= Now, let's allow our module to do some real work. ------------------------------------------------------------------------ #include #include #include /* Handler for command */ void do_hello(char *args) { /* args contains command line with command * name and any spaces after it stripped */ scr_log_print(LPRINT_NORMAL, "Hello, %s!", *args != '\0' ? args : "World"); } /* Register command */ void hello_init(void) { cmd_add("hello", "", 0, 0, do_hello, NULL); } /* Unregister command */ void hello_uninit(void) { cmd_del("hello"); } module_info_t hello_info = { .branch = "dev", .api = 1, .version = "0.0.2", .description = "Hello world module\n" " Provides command /hello", .requires = NULL, .init = hello_init, .uninit = hello_uninit, .next = NULL, } /* The End */ ------------------------------------------------------------------------ Now, compile it and try to load and run /hello with some arguments. Note, that we used one-argument version of command handler, as we have specified no userdata. ========================== Example: completion ========================== Now let's investigate how to provide custom completion to your commands. You can as well use built-in completions, their IDs are listed in compl.h. ------------------------------------------------------------------------ #include #include #include #include static guint hello_cid = 0; /* hello command handler */ void do_hello(char *args) { /* If argument is provided, add it to * completions list. */ if (hello_cid && *args != '\0') compl_add_category_word(hello_cid, args); scr_log_print(LPRINT_NORMAL, "Hello, %s!", *args != '\0' ? args : "World"); } /* Initialization */ void hello_init(void) { /* Obtain handle for our completion * category */ hello_cid = compl_new_category(); if (hello_cid) /* Add known default word to * completion list */ compl_add_category_word(hello_cid, "World"); cmd_add("hello", "", hello_cid, 0, do_hello, NULL); } /* Deinitialization */ void hello_uninit(void) { /* Give back category handle */ if (hello_cid) compl_del_category(hello_cid); cmd_del("hello"); } module_info_t hello_info = { .branch = "dev", .api = 1, .version = "0.0.3", .description = "Hello world module" " Provides command /hello with completion", .requires = NULL, .init = hello_init, .uninit = hello_uninit, .next = NULL, } /* The End */ ------------------------------------------------------------------------ Now you can use completion for hello command. Note, that this code have some serious simplifications, made for simplicity reasons. For now, compl_add_category_word does not checks, if word already exists in completions list (although it is marked as TODO, so, some day it will), so, we should check it ourselves. ===================== Example: hooks ===================== Now let's implement our own beeper. Why may anyone wish to do this? I am not satisfied with default mcabber's builtin beeper flexibility. I wanted beeping on any MUC conference message, not just the ones directed to me. ------------------------------------------------------------------------ #include #include #include #include #include #include #include #include static guint beep_cid = 0; /* Command completion category id */ static guint beep_hid = 0; /* Hook handler id */ /* Event handler */ static guint beep_hh(const gchar *hookname, hk_arg_t *args, gpointer userdata) { /* Check if beeping is enabled */ if (settings_opt_get_int("beep_enable")) scr_Beep(); /* *BEEP*! */ return HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS; } /* beep command handler */ static void do_beep(char *args) { /* Check arguments, and if recognized, * set mcabber option accordingly */ if (!strcmp(args, "enable") || !strcmp(args, "on") || !strcmp(args, "yes") || !strcmp(args, "1")) settings_set(SETTINGS_TYPE_OPTION, "beep_enable", "1"); else if (!strcmp(args, "disable") || !strcmp(args, "off") || !strcmp(args, "no") || !strcmp(args, "0")) settings_set(SETTINGS_TYPE_OPTION, "beep_enable", "0"); /* Output current state, either if state is * changed and if argument is not recognized */ if (settings_opt_get_int("beep_enable")) scr_log_print(LPRINT_NORMAL, "Beep on messages is enabled"); else scr_log_print(LPRINT_NORMAL, "Beep on messages is disabled"); } /* Initialization */ static void beep_init (void) { /* Create completions */ beep_cid = compl_new_category(); if (beep_cid) { compl_add_category_word(beep_cid, "enable"); compl_add_category_word(beep_cid, "disable"); } /* Add command */ cmd_add("beep", "", beep_cid, 0, do_beep, NULL); /* Add handler * We are only interested in incoming message events */ beep_hid = hk_add_handler(beep_hh, HOOK_POST_MESSAGE_IN, G_PRIORITY_DEFAULT_IDLE, NULL); } /* Deinitialization */ void beep_uninit(void) { /* Unregister event handler */ hk_del_handler(HOOK_POST_MESSAGE_IN, beep_hid); /* Unregister command */ cmd_del("beep"); /* Give back completion handle */ if (beep_cid) compl_del_category(beep_cid); } module_info_t beep_info = { .branch = "dev", .api = 1, .version = "0.0.1", .description = "Simple beeper module\n" " Recognizes option beep_enable\n" " Provides command /beep", .requires = NULL, .init = beep_init, .uninit = beep_uninit, .next = NULL, } /* The End */ ------------------------------------------------------------------------ If you use CMake (as do I), corresponding CMakeLists.txt snippet: ------------------------------------------------------------------------ cmake_minimum_required(VERSION 2.6) project(beep C) find_package(PkgConfig REQUIRED) pkg_check_modules(MCABBER REQUIRED mcabber) # this one should be before any target definitions link_directories(${MCABBER_LIBRARY_DIRS}) add_library(beep MODULE beep.c) include_directories(SYSTEM ${MCABBER_INCLUDE_DIRS}) target_link_libraries(beep ${MCABBER_LIBRARIES) include_directories(${beep_SOURCE_DIR} ${beep_BINARY_DIR}) install(TARGETS beep DESTINATION lib/mcabber) ------------------------------------------------------------------------ =========================== Example: dependencies =========================== I will not provide here a complete example of two modules, one of which depends on other, only some use cases. Info struct for module, that depends on two other modules: ------------------------------------------------------------------------ #include const gchar *a_deps[] = { "b", "c", NULL }; module_info_t info_a = { .branch = "dev", .api = 1, .version = NULL, .description = NULL, .requires = a_deps, .init = a_init, .uninit = a_uninit, .next = NULL, }; ------------------------------------------------------------------------ If your module needs to "authenticate" mcabber version too, this can be done in g_module_check_init: ------------------------------------------------------------------------ #include #include #include #include const gchar *g_module_check_init (GModule *module) { char *ver = mcabber_version (); // ver now contains version in format // X.X.X[-xxx][ (XXXXXXXXXXXXX)] const gchar *branch = mcabber_branch; guint api = mcabber_api_version; const gchar *error = NULL; if (...) error = "Incompatible mcabber version"; g_free (ver); return error; } ------------------------------------------------------------------------ Also you can use glib check_init routine to modify module information, that will be checked by mcabber, e.g. if you want your module to always pass mcabber version check, you can assign branch information, obtained from mcabber_... variables to corresponding fields in your struct. Or you can modify your module's dependencies, though direct module_load() will have the same effect, and can be used for optional dependencies, that your module can still work without. Note: remember, that g_module_check_init will be always called, even if later the module will not pass checks, thus: - do not use functions from other modules there; - provide g_module_unload to undo anything, check_init has done. ============== Further ============== As mcabber now uses glib mainloop, you can use glib's event sources, for example, fifo reading already uses GIOChannels for non-blocking IO. You can extend XMPP part of mcabber functionality by providing lm message handlers with high priority and allowing unhandled by your handler messages be taken care by mcabber's handlers on normal priority level. This is where you may need to modify set of advertised supported disco features. Many useful examples can be found in my modules, that can be found at http://isbear.unixzone.org.ua/source. If you think, that your module needs to change something, hardcoded in current implementation - feel free to mail me or join mcabber's MUC room and discuss it - for now we have only implemented things, that we have found necessary for our own modules. Also I am not native English speaker, so, if you find some errors or non-natural constructs in this howto, please, inform me (I will be glad, if you also provide a more suitable version of text in question). -- Myhailo Danylenko -- mailto:isbear@ukrpost.net -- xmpp:isbear@unixzone.org.ua -- Sat, 27 Mar 2010 13:30:00 +0100