# Module Development Guide ## Introduction Modules are the primary way to extend the Prosody Node.js server. They can add new features, handle stanzas, store data, and integrate with external services. ## Module Structure A basic module consists of a JavaScript file that exports an object with specific properties: ```javascript module.exports = { name: 'example', version: '1.0.0', description: 'An example module', author: 'Your Name', dependencies: [], // Other modules this depends on load(module) { // Called when module is loaded }, unload(module) { // Called when module is unloaded } }; ``` ## Module API The `module` object passed to `load()` provides access to the server API: ### Properties - `module.name` - Module name - `module.host` - Virtual host this module is loaded for - `module.api` - API object with helper functions ### API Object ```javascript module.api = { config, // Configuration manager hostManager, // Host manager sessionManager, // Session manager stanzaRouter, // Stanza router storageManager, // Storage manager events, // Event emitter logger, // Logger instance // Helper methods getConfig(key, defaultValue), getGlobalConfig(key, defaultValue), getHostConfig(key, defaultValue), require(moduleName) // Load another module }; ``` ## Examples ### 1. Simple Echo Module ```javascript // modules/mod_echo.js module.exports = { name: 'echo', version: '1.0.0', description: 'Echoes messages back to sender', load(module) { const { logger, stanzaRouter } = module.api; logger.info('Echo module loaded'); module.hook('message', (stanza, session) => { if (stanza.attrs.type === 'chat') { const body = stanza.getChildText('body'); if (body) { // Create echo response const response = stanza.clone(); response.attrs.from = stanza.attrs.to; response.attrs.to = stanza.attrs.from; session.send(response); logger.debug(`Echoed message to ${stanza.attrs.from}`); } } }); } }; ``` ### 2. Message Logger Module ```javascript // modules/mod_message_logger.js module.exports = { name: 'message_logger', version: '1.0.0', description: 'Logs all messages to storage', async load(module) { const { logger, stanzaRouter, storageManager } = module.api; const store = storageManager.getStore(module.host, 'message_log'); logger.info('Message logger module loaded'); stanzaRouter.on('message', async (stanza, session) => { try { const logEntry = { timestamp: Date.now(), from: stanza.attrs.from, to: stanza.attrs.to, type: stanza.attrs.type, body: stanza.getChildText('body') }; const key = `msg_${Date.now()}_${Math.random()}`; await store.set(key, logEntry); logger.debug('Message logged'); } catch (error) { logger.error('Failed to log message:', error); } }); } }; ``` ### 3. User Statistics Module ```javascript // modules/mod_user_stats.js module.exports = { name: 'user_stats', version: '1.0.0', description: 'Tracks user statistics', load(module) { const { logger, sessionManager, storageManager } = module.api; const store = storageManager.getStore(module.host, 'user_stats'); const stats = { totalMessages: 0, totalPresence: 0, onlineUsers: new Set() }; // Track sessions sessionManager.on('session:authenticated', (session) => { stats.onlineUsers.add(session.bareJid); logger.info(`User online: ${session.bareJid} (total: ${stats.onlineUsers.size})`); }); sessionManager.on('session:closed', (session) => { stats.onlineUsers.delete(session.bareJid); logger.info(`User offline: ${session.bareJid} (total: ${stats.onlineUsers.size})`); }); // Track stanzas module.on('message', () => { stats.totalMessages++; }); module.on('presence', () => { stats.totalPresence++; }); // Expose stats via IQ module.hook('iq', (stanza, session) => { if (stanza.attrs.type === 'get') { const query = stanza.getChild('query'); if (query && query.attrs.xmlns === 'http://example.com/stats') { const response = new ltx.Element('iq', { type: 'result', id: stanza.attrs.id, to: stanza.attrs.from }).c('query', { xmlns: 'http://example.com/stats' }) .c('stat', { name: 'messages' }).t(stats.totalMessages.toString()).up() .c('stat', { name: 'presence' }).t(stats.totalPresence.toString()).up() .c('stat', { name: 'online' }).t(stats.onlineUsers.size.toString()); session.send(response); return true; // Handled } } }); } }; ``` ### 4. Rate Limiting Module ```javascript // modules/mod_rate_limit.js const { RateLimiterMemory } = require('rate-limiter-flexible'); module.exports = { name: 'rate_limit', version: '1.0.0', description: 'Rate limits stanzas per user', load(module) { const { logger, stanzaRouter, config } = module.api; const limiter = new RateLimiterMemory({ points: module.api.getConfig('points', 10), duration: module.api.getConfig('duration', 1) }); // Add filter to stanza router stanzaRouter.addFilter(async (stanza, session) => { if (!session || !session.bareJid) return true; try { await limiter.consume(session.bareJid); return true; // Allow stanza } catch (error) { logger.warn(`Rate limit exceeded for ${session.bareJid}`); return false; // Block stanza } }); logger.info('Rate limiting enabled'); } }; ``` ## Hooks and Events ### Stanza Events - `message` - Message stanza received - `presence` - Presence stanza received - `iq` - IQ stanza received - `iq:get` - IQ get request - `iq:set` - IQ set request - `iq:result` - IQ result - `iq:error` - IQ error ### Session Events - `session:created` - New session created - `session:authenticated` - Session authenticated - `session:closed` - Session closed ### Server Events - `server:started` - Server started - `server:stopped` - Server stopped ## Storage Modules can store persistent data: ```javascript const store = storageManager.getStore(module.host, 'mydata'); // Store data await store.set('key', { data: 'value' }); // Retrieve data const data = await store.get('key'); // Delete data await store.delete('key'); // Find data const results = await store.find(item => item.type === 'important'); ``` ## Configuration Modules can access configuration: ```javascript // Module-specific config from moduleConfig section const enabled = module.api.getConfig('enabled', true); // Global config const domain = module.api.getGlobalConfig('server.domain'); // Host-specific config const modules = module.api.getHostConfig('modules', []); ``` ## Best Practices 1. **Error Handling**: Always use try-catch blocks 2. **Logging**: Log important events and errors 3. **Cleanup**: Implement proper cleanup in `unload()` 4. **Performance**: Avoid blocking operations 5. **Testing**: Write tests for your modules 6. **Documentation**: Document module configuration ## Testing ```javascript // test/mod_example.test.js const ModuleManager = require('../src/core/module-manager'); describe('Example Module', () => { it('should load successfully', async () => { // Test module loading }); it('should handle messages', async () => { // Test message handling }); }); ``` ## Publishing To share your module: 1. Create a repository 2. Add proper documentation 3. Include example configuration 4. Publish to npm (optional) ## Resources - [XMPP RFCs](https://xmpp.org/rfcs/) - [XMPP Extensions (XEPs)](https://xmpp.org/extensions/) - [Module Examples](examples/modules/)