Implementing Alicq module: Step by Step

Author Ihar Viarheichyk

Create simple extension module for Alicq, which monitors changes of contact status and save them to file.

Content

I. Step 1: Creating module file

Alicq module is just a Tcl script, which is loaded by Alicq, and can access to data of other modules, hook Alicq events etc.

First, make directory modules in your alicq base directory (it is ~/.alicq in Unix, and %USERPROFILE%/alicq in Windows). Alicq will look for user-specific modules there.

In modules directory create file statusmonitor.tcl. Content of this file can be any valid Tcl script. Let make it just say that module is loaded:
		Event Log 0 "My first module us loaded"
	
That's it, simpliest module is ready and can be included into ~/.alicq/alicqrc fiile:
		module statusmonitor
	
Run alicq and look at alicq.log file in alicq base directory. It should contain record like:
	[25.03.2004 10:45:29]: My first module is loaded
	

II. Step 2: Select contacts to monitor

Ok, simpliest module is working, not it is time to make it more usefll.

Our purpose it to monitor changes of contacts. When contact changes it's status, icq module updates corresponding Status field of object's variable.

First, we should find the objects we have to monitor. Any module can create objects of different purposes, but we need only ICQ contacts. They can be selected using select helper command.
		# Find all contacts and invoke MonitorContact for each of
		# them.
		proc InitMonitoring {} {
			foreach contact [select Contact:ICQ] {
				MonitorContact $contact
			}
		}
		
		# Fake monitor installer - just log it was invoked.
		proc MonitorContact {contact} {
			Event Log 0 "Monitoring contact $contact"
		}
		
		InitMonitoring
		Event Log 0 "My first module is loaded"
	
This module will work, but there is a problem. If contacts are loaded after this module, no contacts will be selected in InitMonitor procedre. There is a way to load the module after contacts, but this is not best solution.

Normally, module should not depend on order of loading. There is Alicq event ConfigLoaded, which is sent after startup file was read and processed. At this moment all modules and contacts are loaded. Properly changed example is:
		# Find all contacts and invoke MonitorContact for each of
		# them.
		proc InitMonitoring {} {
			foreach contact [select Contact:ICQ] {
				MonitorContact $contact
			}
		}
		
		# Fake monitor installer - just log it was invoked.
		proc MonitorContact {contact} {
			Event Log 0 "Monitoring contact $contact"
		}
		
		hook ConfigLoaded [namespace current]::InitMonitoring
		Event Log 0 "My first module is loaded"
	
Now, MonitorContact will be invoked for all existing contacts. Seems everything is fine. But wait - what if contact is being added during work? It had not existed when InitMonitoring was invoked, and no monitoring is installed for it. Module should monitor creation of new contacts:
		# Find all contacts and invoke MonitorContact for each of
		# them.
		proc InitMonitoring {} {
			foreach contact [select Contact:ICQ] {
				MonitorContact $contact
			}
			# Monitor new objects
			hook New:Contact:ICQ:* [namespace current]::MonitorContact
		}
		
		# Fake monitor installer - just log it was invoked.
		proc MonitorContact {contact} {
			Event Log 0 "Monitoring contact $contact"
		}
		
		hook ConfigLoaded [namespace current]::InitMonitoring
		Event Log 0 "My first module is loaded"
	
Ok, now addition of contacts is monitored, but what about deletion of ones? It can be done, but there is no need in this for our purpose: status of deleted contact never changes and need not to be monitored.

III. Step 3: Status monitoring

Let's replace MonitorContact procedure stub for real monitoring example. This procedure is invoked with one agrument: unique object identifier (uid). This identifier can be converted into corresponding variable name using ref command. Having variable name, Tcl standard command trace can be used for monitoring.
		# Find all contacts and invoke MonitorContact for each of
		# them.
		proc InitMonitoring {} {
			foreach contact [select Contact:ICQ] {
				MonitorContact $contact
			}
			hook New:Contact:ICQ:* [namespace current]::MonitorContact
		}

		proc MonitorContact {contact} {
			set ref [ref $contact]
			trace variable ${ref}(Status) w [nc StatusChanged $contact]
			Event Log 0 "Monitoring contact $contact"
		}

		proc StatusChanged {contact ref field args} {
			upvar 1 ${ref}($field) status
			Event Log 0 "Contact $contact changed status to $status"
		}

		hook ConfigLoaded [namespace current]::InitMonitoring
		Event Log 0 "My first module is loaded"
	
This example monitors contacts status and logs it in Alicq log file. It can be not very conveniet, because log file can contain a lot of other information. It is better to save log into separate file.
		# Find all contacts and invoke MonitorContact for each of
		# them.
		variable logname status.log
		proc InitMonitoring {} {
			foreach contact [select Contact:ICQ] {
				MonitorContact $contact
			}
			hook New:Contact:ICQ:* [namespace current]::MonitorContact
		}

		proc MonitorContact {contact} {
			set ref [ref $contact]
			trace variable ${ref}(Status) w [nc StatusChanged $contact]
			Event Log 0 "Monitoring contact $contact"
		}

		proc StatusChanged {contact ref field args} {
			upvar 1 ${ref}($field) status
			variable logname
			if {[catch {
				set fd [open $logname a+]
				puts $fd "$contact $status"
				close $fd
			} reason]} { Log 0 "Can not log status of $contact: $reason"}
		}

		hook ConfigLoaded [namespace current]::InitMonitoring
		Event Log 0 "My first module is loaded"
	

IV. Step 4: Make module configurable

The monitoring module now have no configurable parameters. And possibly module of such purpose does not need them. However, let's add some of them just to demonstrate how modules metadata is used in Alicq.

Each module and module child namespaces can have metadata, containing module description, name of author, etc., as well as configurable parameters metadata.

What configuration parameters can be usefull? There are thos of them: name of log file and monitoring active/passive flag. User should be able turn on or off monitoring from main menu and configuration dialog, and change file name from configuration dialog.
		namespace eval meta {
			set description "Log changes of contact status into file"
			set name "Status logger"
			
			array set active { 
				type boolean 
				default no
				menu {Tools "Monitor Status"}
				description "Activate status logging"
			}

			array set logname {
				type file
				default status.log
				description "Log file name"
			}
		}

		# Find all contacts and invoke MonitorContact for each of
		# them.
		proc InitMonitoring {} {
			foreach contact [select Contact:ICQ] {
				MonitorContact $contact
			}
			hook New:Contact:ICQ:* [namespace current]::MonitorContact
		}

		proc MonitorContact {contact} {
			set ref [ref $contact]
			trace variable ${ref}(Status) w [nc StatusChanged $contact]
			Event Log 0 "Monitoring contact $contact"
		}

		proc StatusChanged {contact ref field args} {
			variable logname
			variable active
			# If monitoring is inactive, return without saving
			if {![string is true $active]} return
			upvar 1 ${ref}($field) status
			if {[catch {
				set fd [open $logname a+]
				puts $fd "$contact $status"
				close $fd
			} reason]} { Log 0 "Can not log status of $contact: $reason"}
		}

		hook ConfigLoaded [namespace current]::InitMonitoring
		Event Log 0 "My first module is loaded"
	
After Alicq is loaded, new item, Tools -> Monitor Status appear in main menu, as well as page "Status logger" in configuration dialog.

V. Step 5: Select contacts to monitor

Now module can log status changes, and can be configured. What enhancements can be usefull? If contact list is big, it is better to monitor only some contacts, not all of them. For example, only users of group monitor should be monitored.
		proc StatusChanged {contact ref field args} {
			variable logname
			variable active
			# If monitoring is inactive, return without saving
			if {![string is true $active]} return
			# Check, if contact is member of monitor group
			if {![info exists ${ref}(Groups)] ||
			     [lsearch [set ${ref}(Groups)] monitor]==-1} return
			upvar 1 ${ref}($field) status
			if {[catch {
				set fd [open $logname a+]
				puts $fd "$contact $status"
				close $fd
			} reason]} { Log 0 "Can not log status of $contact: $reason"}
		}
	
Now you can create group monitor, if it does not exist, and copy contacts you want to monitor there.