326
docs/MODULE_DEVELOPMENT.md
Archivo normal
326
docs/MODULE_DEVELOPMENT.md
Archivo normal
@@ -0,0 +1,326 @@
|
||||
# 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/)
|
||||
Referencia en una nueva incidencia
Block a user