7.9 KiB
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 namemodule.host- Virtual host this module is loaded formodule.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 receivedpresence- Presence stanza receivediq- IQ stanza receivediq:get- IQ get requestiq:set- IQ set requestiq:result- IQ resultiq:error- IQ error
Session Events
session:created- New session createdsession:authenticated- Session authenticatedsession:closed- Session closed
Server Events
server:started- Server startedserver: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
- Error Handling: Always use try-catch blocks
- Logging: Log important events and errors
- Cleanup: Implement proper cleanup in
unload() - Performance: Avoid blocking operations
- Testing: Write tests for your modules
- 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:
- Create a repository
- Add proper documentation
- Include example configuration
- Publish to npm (optional)