Files
prosody-nodejs/docs/MODULE_DEVELOPMENT.md
2025-12-27 03:39:14 +01:00

7.9 KiB

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:

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

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

// 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

// 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

// 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

// 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:

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:

// 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

// 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