Dynamic script loading from files and npm packages, plus middleware system for customizing message processing pipeline.
Hubot supports dynamic loading of bot functionality from local files and external npm packages.
/**
* Load a single script file
* Supports .js, .mjs, and .ts extensions
* @param filepath - Full path to script file
* @param filename - Name of the file for logging purposes
*/
async loadFile(filepath: string, filename: string): Promise<any>;
/**
* Load all scripts from a directory
* Recursively loads all supported script files
* @param path - Directory path containing scripts
*/
async load(path: string): Promise<any[]>;
/**
* Load external scripts from npm packages
* @param packages - Array of npm package names or object with package configs
*/
async loadExternalScripts(packages: string[] | object): Promise<void>;Usage Examples:
import { Robot } from "hubot";
const robot = new Robot("Shell");
// Load single script file
await robot.loadFile("./scripts/custom-commands.js", "custom-commands.js");
// Load all scripts from directory
await robot.load("./scripts");
// Load external npm packages
await robot.loadExternalScripts([
"hubot-help",
"hubot-diagnostics",
"@hubot-friends/hubot-pugme",
"hubot-rules"
]);
await robot.run();Scripts are modules that export a function which receives the robot instance and adds listeners.
/**
* Standard script export function
* @param robot - Robot instance to configure
*/
export default function(robot: Robot): void {
// Add listeners, configure robot, etc.
}
// CommonJS format
module.exports = function(robot) {
// Script functionality
};Example Script:
// scripts/weather.js
export default function(robot) {
// Simple weather command
robot.respond(/weather (.+)/i, async (res) => {
const location = res.match[1];
try {
const weather = await getWeather(location);
res.send(`Weather in ${location}: ${weather.description}, ${weather.temp}°C`);
} catch (error) {
res.send(`Sorry, couldn't get weather for ${location}`);
}
});
// Help documentation
robot.commands.push("weather <location> - Get weather for location");
}
async function getWeather(location) {
// Weather API integration
const response = await fetch(`https://api.weather.com/v1/current?location=${location}`);
return response.json();
}Middleware provides hooks into the message processing pipeline for custom behavior.
/**
* Register listener middleware (runs before listeners are matched)
* @param middleware - Middleware function
*/
listenerMiddleware(middleware: MiddlewareFunction): void;
/**
* Register response middleware (runs before responses are sent)
* @param middleware - Middleware function
*/
responseMiddleware(middleware: MiddlewareFunction): void;
/**
* Register receive middleware (runs when messages are first received)
* @param middleware - Middleware function
*/
receiveMiddleware(middleware: MiddlewareFunction): void;
/**
* Middleware function signature
* @param context - Middleware context object
* @param next - Function to call to continue processing
* @param done - Function to call when middleware is complete
*/
type MiddlewareFunction = (context: any, next: () => void, done: () => void) => void;Usage Examples:
// Logging middleware
robot.receiveMiddleware((context, next, done) => {
robot.logger.info(`Received message: ${context.response.message.text}`);
next(); // Continue processing
done(); // Mark middleware complete
});
// Authentication middleware
robot.listenerMiddleware((context, next, done) => {
const user = context.response.message.user;
if (context.listener.options.requireAuth && !user.get('authenticated')) {
context.response.send("You must authenticate first!");
done(); // Stop processing - don't call next()
return;
}
next(); // Continue to listener
done(); // Mark middleware complete
});
// Rate limiting middleware
const rateLimits = new Map();
robot.responseMiddleware((context, next, done) => {
const userId = context.response.message.user.id;
const now = Date.now();
const lastMessage = rateLimits.get(userId) || 0;
if (now - lastMessage < 1000) { // 1 second rate limit
done(); // Skip response - don't call next()
return;
}
rateLimits.set(userId, now);
next(); // Send response
done(); // Mark middleware complete
});
// Response transformation middleware
robot.responseMiddleware((context, next, done) => {
// Add emoji to all responses
if (context.string) {
context.string = `🤖 ${context.string}`;
}
next(); // Continue processing
done(); // Mark middleware complete
});The context object passed to middleware contains information about the current message processing.
interface MiddlewareContext {
response: Response; // Response object
listener?: Listener; // Current listener (listener middleware only)
string?: string; // Response string (response middleware only)
}External scripts can be configured through package.json and environment variables.
// package.json
{
"dependencies": {
"hubot-help": "^1.0.0",
"hubot-diagnostics": "^1.0.0"
},
"hubot": {
"external-scripts": [
"hubot-help",
"hubot-diagnostics"
]
}
}
// Load from package.json
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
await robot.loadExternalScripts(packageJson.hubot['external-scripts']);Scripts should provide help documentation that integrates with hubot's help system.
// Add commands to robot.commands array
robot.commands.push("ping - Reply with pong");
robot.commands.push("time - Display current time");
robot.commands.push("echo <message> - Echo back the message");
// Help command will automatically display all commands
robot.respond(/help$/i, (res) => {
const commands = robot.commands.map(cmd => `• ${cmd}`).join('\n');
res.send(`Available commands:\n${commands}`);
});Best practices for error handling in scripts and middleware.
// Script-level error handling
export default function(robot) {
robot.respond(/risky command/i, async (res) => {
try {
const result = await riskyOperation();
res.send(`Success: ${result}`);
} catch (error) {
robot.logger.error('Risky command failed:', error);
res.send("Sorry, something went wrong!");
}
});
// Global error handler for this script
robot.error((error) => {
robot.logger.error('Script error:', error);
});
}
// Middleware error handling
robot.listenerMiddleware((context, next, done) => {
try {
// Middleware logic
next(); // Continue processing
done(); // Mark complete
} catch (error) {
context.response.robot.logger.error('Middleware error:', error);
done(); // Stop processing on error - don't call next()
}
});Loading scripts based on environment or configuration.
// Load different scripts for different environments
if (process.env.NODE_ENV === 'production') {
await robot.loadExternalScripts(['hubot-monitoring', 'hubot-alerts']);
} else {
await robot.loadExternalScripts(['hubot-diagnostics', 'hubot-dev-tools']);
}
// Load scripts based on adapter
if (robot.adapterName === 'Slack') {
await robot.loadExternalScripts(['hubot-slack-reactions']);
} else if (robot.adapterName === 'Discord') {
await robot.loadExternalScripts(['hubot-discord-features']);
}The Middleware class manages middleware stacks for different processing stages.
/**
* Middleware stack management
* @param robot - Robot instance
*/
class Middleware {
constructor(robot: Robot);
robot: Robot;
stack: MiddlewareFunction[];
/**
* Execute middleware stack with context
* @param context - Context object for middleware
* @returns Promise resolving to boolean (true to continue, false to stop)
*/
async execute(context: MiddlewareContext): Promise<boolean>;
/**
* Register new middleware function
* @param middleware - Function to add to stack
*/
register(middleware: MiddlewareFunction): void;
}type MiddlewareFunction = (context: any, next: () => void, done: () => void) => void;
interface MiddlewareContext {
response: Response;
listener?: Listener;
string?: string;
}
interface ScriptModule {
default?: (robot: Robot) => void;
(robot: Robot): void; // CommonJS export
}
interface ExternalScriptConfig {
'external-scripts': string[];
}