Replace dual message-channel + RPC setup with a single registerPluginProtocol path, unified sdk.channels (register/request/event/broadcast), RPC keepalive (__p2ns.ping/pong), and core request lifecycle on RPC open. Update global.profile and example.plugin, admin Plugin RPC stats, docs, and test:plugin-rpc. Breaking: upgrade all peers together; no legacy adapters.
353 lines
11 KiB
JavaScript
353 lines
11 KiB
JavaScript
/**
|
||
* Example Plugin
|
||
*
|
||
* This is a comprehensive template plugin for P2NS.
|
||
* Use this as a starting point when creating new plugins.
|
||
*
|
||
* This plugin demonstrates:
|
||
* - Basic request handling
|
||
* - Static file serving from www/ directory
|
||
* - Simple API endpoint
|
||
* - Plugin lifecycle hooks
|
||
* - Admin settings integration
|
||
* - Admin actions integration
|
||
* - Using settings in handler and frontend
|
||
*/
|
||
|
||
const sdk = require('../../includes/plugins/sdk');
|
||
|
||
// Store action results for display
|
||
let lastActionResult = null;
|
||
let actionHistory = [];
|
||
|
||
/**
|
||
* Plugin Handler
|
||
*
|
||
* Handles all HTTP requests for example.plugin
|
||
*
|
||
* @param {Object} req - Node.js HTTP request object
|
||
* @param {Object} res - Node.js HTTP response object
|
||
* @returns {Promise<boolean>}
|
||
* - true: Request was handled by this plugin
|
||
* - false: Request should fall back to static file serving
|
||
*/
|
||
async function handler(req, res) {
|
||
try {
|
||
// Parse request using SDK router
|
||
// This extracts the path and query parameters from the URL
|
||
const { path, query, method } = sdk.router.parseRequest(req);
|
||
|
||
// Handle root path - return false to serve index.html from www/ directory
|
||
if (path === '' || path === '/') {
|
||
return false;
|
||
}
|
||
|
||
// Example: Handle a simple API endpoint that uses settings
|
||
// GET /api/hello?name=World
|
||
if (path === 'api/hello' && method === 'GET') {
|
||
// Get the greeting prefix from settings
|
||
const greetingPrefix = await sdk.admin.getSetting('exampleString', 'Hello');
|
||
const name = query.name || 'World';
|
||
|
||
// Use the boolean setting to determine if we should include timestamp
|
||
const includeTimestamp = await sdk.admin.getSetting('exampleBoolean', false);
|
||
|
||
const response = {
|
||
message: `${greetingPrefix}, ${name}!`,
|
||
};
|
||
|
||
if (includeTimestamp) {
|
||
response.timestamp = new Date().toISOString();
|
||
}
|
||
|
||
return sdk.router.json(res, response);
|
||
}
|
||
|
||
// Example: Handle another API endpoint that uses settings
|
||
// GET /api/info
|
||
if (path === 'api/info' && method === 'GET') {
|
||
// Get all settings to show how they're being used
|
||
const exampleString = await sdk.admin.getSetting('exampleString', 'Hello, World!');
|
||
const exampleNumber = await sdk.admin.getSetting('exampleNumber', 42);
|
||
const exampleBoolean = await sdk.admin.getSetting('exampleBoolean', false);
|
||
const exampleSelect = await sdk.admin.getSetting('exampleSelect', 'option1');
|
||
|
||
return sdk.router.json(res, {
|
||
plugin: 'example.plugin',
|
||
version: '1.0.0',
|
||
description: 'A comprehensive template plugin demonstrating admin settings and actions',
|
||
dnsReady: sdk.utils.isDNSReady(),
|
||
connectedPeers: sdk.state.connectedPeers,
|
||
settings: {
|
||
exampleString,
|
||
exampleNumber,
|
||
exampleBoolean,
|
||
exampleSelect
|
||
},
|
||
settingsInUse: {
|
||
greetingPrefix: exampleString,
|
||
multiplier: exampleNumber,
|
||
showTimestamps: exampleBoolean,
|
||
mode: exampleSelect
|
||
}
|
||
});
|
||
}
|
||
|
||
// Example: Get user's public key
|
||
// GET /api/public-key
|
||
if (path === 'api/public-key' && method === 'GET') {
|
||
const publicKey = sdk.state.localPeerId;
|
||
return sdk.router.json(res, {
|
||
publicKey: publicKey || null,
|
||
hasIdentity: !!publicKey
|
||
});
|
||
}
|
||
|
||
// Example: API endpoint that uses number setting
|
||
// GET /api/calculate?value=10
|
||
if (path === 'api/calculate' && method === 'GET') {
|
||
const exampleNumber = await sdk.admin.getSetting('exampleNumber', 42);
|
||
const inputValue = parseFloat(query.value) || 0;
|
||
const result = inputValue * exampleNumber;
|
||
|
||
return sdk.router.json(res, {
|
||
input: inputValue,
|
||
multiplier: exampleNumber,
|
||
result: result,
|
||
calculation: `${inputValue} × ${exampleNumber} = ${result}`
|
||
});
|
||
}
|
||
|
||
// Example: API endpoint that uses select setting
|
||
// GET /api/mode
|
||
if (path === 'api/mode' && method === 'GET') {
|
||
const exampleSelect = await sdk.admin.getSetting('exampleSelect', 'option1');
|
||
|
||
const modeDescriptions = {
|
||
option1: 'Standard mode - basic functionality',
|
||
option2: 'Enhanced mode - additional features',
|
||
option3: 'Advanced mode - full feature set'
|
||
};
|
||
|
||
return sdk.router.json(res, {
|
||
mode: exampleSelect,
|
||
description: modeDescriptions[exampleSelect] || 'Unknown mode',
|
||
availableModes: ['option1', 'option2', 'option3']
|
||
});
|
||
}
|
||
|
||
// Example: Get action history
|
||
// GET /api/actions
|
||
if (path === 'api/actions' && method === 'GET') {
|
||
return sdk.router.json(res, {
|
||
lastResult: lastActionResult,
|
||
history: actionHistory.slice(-10) // Last 10 actions
|
||
});
|
||
}
|
||
|
||
// Example: Get all current settings
|
||
// GET /api/settings
|
||
if (path === 'api/settings' && method === 'GET') {
|
||
const allSettings = await sdk.admin.getAllSettings();
|
||
return sdk.router.json(res, {
|
||
settings: allSettings,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
}
|
||
|
||
// Return false for all other routes to allow static file serving
|
||
// This means CSS, JS, images, etc. will be served from www/ directory
|
||
return false;
|
||
} catch (err) {
|
||
// Catch any unexpected errors
|
||
sdk.log.error('example.plugin', `Error handling request: ${err.message}`);
|
||
return sdk.router.error(res, 'Internal Server Error', 500);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Plugin Initialization Hook
|
||
*
|
||
* Called automatically when the plugin is loaded by the P2NS system.
|
||
* Use this to initialize resources, set up timers, pre-load data, etc.
|
||
*/
|
||
async function onInit() {
|
||
sdk.log.info('example.plugin', 'Plugin initialized');
|
||
|
||
sdk.channels.register('rpc-demo', {
|
||
autoReconnect: true,
|
||
methods: {
|
||
'demo.ping': async (value) => ({
|
||
pong: true,
|
||
echo: value,
|
||
at: new Date().toISOString()
|
||
})
|
||
}
|
||
});
|
||
sdk.log.info('example.plugin', 'Registered RPC demo.ping on rpc-demo protocol');
|
||
|
||
// Example: Check if DNS service is ready
|
||
if (sdk.utils.isDNSReady()) {
|
||
sdk.log.info('example.plugin', 'DNS service is ready');
|
||
} else {
|
||
sdk.log.warn('example.plugin', 'DNS service is not ready yet');
|
||
}
|
||
|
||
// Register actions that can be called from the admin panel
|
||
sdk.admin.registerAction('testAction', async (params) => {
|
||
sdk.log.info('example.plugin', `Test action called with params: ${JSON.stringify(params)}`);
|
||
|
||
// Get current settings to use in action
|
||
const exampleString = await sdk.admin.getSetting('exampleString', 'Hello, World!');
|
||
const exampleNumber = await sdk.admin.getSetting('exampleNumber', 42);
|
||
const exampleBoolean = await sdk.admin.getSetting('exampleBoolean', false);
|
||
const exampleSelect = await sdk.admin.getSetting('exampleSelect', 'option1');
|
||
|
||
const result = {
|
||
success: true,
|
||
message: 'Test action executed successfully!',
|
||
timestamp: new Date().toISOString(),
|
||
params: params || {},
|
||
settingsUsed: {
|
||
greeting: exampleString,
|
||
multiplier: exampleNumber,
|
||
featureEnabled: exampleBoolean,
|
||
mode: exampleSelect
|
||
},
|
||
calculation: `Using multiplier ${exampleNumber}: ${(params?.value || 10) * exampleNumber}`
|
||
};
|
||
|
||
// Store result for API access
|
||
lastActionResult = result;
|
||
actionHistory.push({
|
||
timestamp: new Date().toISOString(),
|
||
action: 'testAction',
|
||
params,
|
||
result
|
||
});
|
||
|
||
// Keep history limited to last 50 actions
|
||
if (actionHistory.length > 50) {
|
||
actionHistory = actionHistory.slice(-50);
|
||
}
|
||
|
||
return result;
|
||
}, {
|
||
label: 'Test Action',
|
||
description: 'A test action that uses current settings and demonstrates the action system',
|
||
icon: '🧪'
|
||
});
|
||
|
||
// Register a more complex action that demonstrates different capabilities
|
||
sdk.admin.registerAction('resetData', async (params) => {
|
||
sdk.log.info('example.plugin', 'Reset data action called');
|
||
|
||
// Clear action history
|
||
actionHistory = [];
|
||
lastActionResult = null;
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Action history and cached data have been reset',
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
}, {
|
||
label: 'Reset Data',
|
||
description: 'Clear action history and reset cached data',
|
||
icon: '🔄'
|
||
});
|
||
|
||
// Register a calculation action that uses the number setting
|
||
sdk.admin.registerAction('calculate', async (params) => {
|
||
const exampleNumber = await sdk.admin.getSetting('exampleNumber', 42);
|
||
const inputValue = params?.value ? parseFloat(params.value) : 10;
|
||
const result = inputValue * exampleNumber;
|
||
|
||
const calcResult = {
|
||
success: true,
|
||
message: `Calculation completed using multiplier ${exampleNumber}`,
|
||
input: inputValue,
|
||
multiplier: exampleNumber,
|
||
result: result,
|
||
calculation: `${inputValue} × ${exampleNumber} = ${result}`,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
actionHistory.push({
|
||
timestamp: new Date().toISOString(),
|
||
action: 'calculate',
|
||
params,
|
||
result: calcResult
|
||
});
|
||
|
||
if (actionHistory.length > 50) {
|
||
actionHistory = actionHistory.slice(-50);
|
||
}
|
||
|
||
return calcResult;
|
||
}, {
|
||
label: 'Calculate',
|
||
description: 'Perform a calculation using the number setting as multiplier',
|
||
icon: '🔢'
|
||
});
|
||
|
||
// Register settings that will appear in the admin panel
|
||
sdk.admin.registerSetting('exampleString', {
|
||
type: 'string',
|
||
label: 'Greeting Prefix',
|
||
description: 'The greeting prefix used in the /api/hello endpoint (e.g., "Hello", "Hi", "Greetings")',
|
||
default: 'Hello, World!'
|
||
});
|
||
|
||
sdk.admin.registerSetting('exampleNumber', {
|
||
type: 'number',
|
||
label: 'Multiplier',
|
||
description: 'A number used as a multiplier in calculations (used in /api/calculate and actions)',
|
||
default: 42,
|
||
min: 1,
|
||
max: 1000
|
||
});
|
||
|
||
sdk.admin.registerSetting('exampleBoolean', {
|
||
type: 'boolean',
|
||
label: 'Include Timestamps',
|
||
description: 'When enabled, API responses will include timestamp fields',
|
||
default: false
|
||
});
|
||
|
||
sdk.admin.registerSetting('exampleSelect', {
|
||
type: 'select',
|
||
label: 'Operation Mode',
|
||
description: 'Select the operational mode for the plugin',
|
||
default: 'option1',
|
||
options: [
|
||
{ value: 'option1', label: 'Standard Mode' },
|
||
{ value: 'option2', label: 'Enhanced Mode' },
|
||
{ value: 'option3', label: 'Advanced Mode' }
|
||
]
|
||
});
|
||
|
||
// Log initial settings
|
||
const initialSettings = await sdk.admin.getAllSettings();
|
||
sdk.log.info('example.plugin', `Initial settings loaded: ${JSON.stringify(initialSettings)}`);
|
||
}
|
||
|
||
/**
|
||
* Plugin Shutdown Hook
|
||
*
|
||
* Called automatically when the plugin is unloaded by the P2NS system.
|
||
* Use this to clean up resources, close connections, stop timers, etc.
|
||
*/
|
||
async function onShutdown() {
|
||
sdk.log.info('example.plugin', 'Plugin shutting down');
|
||
// Add cleanup code here if needed
|
||
}
|
||
|
||
// Export the plugin interface
|
||
module.exports = {
|
||
handler, // HTTP request handler - processes all incoming requests
|
||
onInit, // Called when plugin loads
|
||
onShutdown // Called when plugin unloads
|
||
};
|
||
|