253 lines
8.8 KiB
JavaScript
253 lines
8.8 KiB
JavaScript
/**
|
|
* Global Profile Plugin
|
|
*
|
|
* Provides universal user identity system across all P2NS plugins with:
|
|
* - HyperDB-backed profile storage with P2P replication
|
|
* - Multi-size avatar storage via HyperDB (base64 encoded)
|
|
* - Extensible field system for plugin-registered custom fields
|
|
* - Real-time profile updates via WebSocket
|
|
*/
|
|
|
|
const sdk = require('../../includes/plugins/sdk');
|
|
|
|
// Import modules
|
|
const { getProfileFromDB, saveProfileToDB } = require('./database');
|
|
const { broadcastProfileUpdate, setupWebSocketHandlers, teardownWebSocketHandlers, getReplicationStatus } = require('./websocket');
|
|
const { setupDatabaseWatcher, teardownDatabaseWatcher } = require('./watcher');
|
|
const { handleProfileRoutes } = require('./routes/profile');
|
|
const { handleAvatarRoutes } = require('./routes/avatar');
|
|
const { handleProfilesRoutes } = require('./routes/profiles');
|
|
const { handleFieldsRoutes } = require('./routes/fields');
|
|
const { handleStatsRoutes } = require('./routes/stats');
|
|
const { handleDocsRoutes } = require('./routes/docs');
|
|
|
|
let peerConnectedHandler = null;
|
|
let peerDisconnectedHandler = null;
|
|
|
|
/**
|
|
* Plugin Handler
|
|
*/
|
|
async function handler(req, res) {
|
|
try {
|
|
// Handle CORS (including OPTIONS preflight)
|
|
if (sdk.router.handleCORS(req, res, { origin: '*' })) {
|
|
return true; // OPTIONS request handled
|
|
}
|
|
|
|
const { path, query, method } = sdk.router.parseRequest(req);
|
|
|
|
// Handle root path - serve index.html
|
|
if (path === '' || path === '/') {
|
|
return false;
|
|
}
|
|
|
|
// Try each route module in order
|
|
// Avatar routes must come BEFORE profile routes to avoid conflicts with /api/profile/:peerId
|
|
const routeModules = [
|
|
{ name: 'avatar', handler: handleAvatarRoutes },
|
|
{ name: 'profile', handler: handleProfileRoutes },
|
|
{ name: 'profiles', handler: handleProfilesRoutes },
|
|
{ name: 'fields', handler: handleFieldsRoutes },
|
|
{ name: 'stats', handler: handleStatsRoutes },
|
|
{ name: 'docs', handler: handleDocsRoutes }
|
|
];
|
|
|
|
for (const routeModule of routeModules) {
|
|
const result = await routeModule.handler(req, res, path, query, method);
|
|
if (result !== false) {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Return false for all other routes to allow static file serving
|
|
return false;
|
|
} catch (err) {
|
|
sdk.log.error('global.profile', `Error handling request: ${err.message}`);
|
|
return sdk.router.error(res, 'Internal Server Error', 500);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Plugin Initialization Hook
|
|
*/
|
|
async function onInit() {
|
|
// Store the plugin domain globally for easy access
|
|
global.PLUGIN_DOMAIN_GLOBAL = process.env.PLUGIN_DOMAIN || 'global.profile';
|
|
|
|
sdk.log.info('global.profile', 'Initializing Global Profile plugin...');
|
|
|
|
try {
|
|
// Database is opened by plugin-handler before onInit; a single ready() is enough.
|
|
try {
|
|
await sdk.db.ready(5000);
|
|
sdk.log.info('global.profile', 'Database ready');
|
|
} catch (err) {
|
|
sdk.log.warn('global.profile', `Database not ready during init: ${err.message}`);
|
|
}
|
|
|
|
// Avatars are now stored in HyperDB, so no separate drive initialization needed
|
|
sdk.log.info('global.profile', 'Avatars will be stored in HyperDB (automatic replication)');
|
|
|
|
try {
|
|
sdk.channels.register('profile-updates', {
|
|
methods: {
|
|
'profile.update': async (data, { peerId }) => {
|
|
sdk.log.debug('global.profile', `Received profile update from peer ${peerId.substring(0, 16)}... via RPC`);
|
|
sdk.websocket.broadcast({
|
|
...data,
|
|
timestamp: data.timestamp || Date.now()
|
|
});
|
|
return null;
|
|
}
|
|
},
|
|
onPeerOpen: (peerId) => {
|
|
sdk.log.debug('global.profile', `Peer ${peerId.substring(0, 16)}... connected to profile-updates RPC`);
|
|
},
|
|
onPeerClose: (peerId) => {
|
|
sdk.log.debug('global.profile', `Peer ${peerId.substring(0, 16)}... disconnected from profile-updates RPC`);
|
|
}
|
|
});
|
|
sdk.log.info('global.profile', 'P2P RPC protocol for profile updates registered');
|
|
} catch (err) {
|
|
sdk.log.error('global.profile', `Error creating profile-updates channel: ${err.message}`);
|
|
}
|
|
|
|
// Setup WebSocket handlers
|
|
setupWebSocketHandlers();
|
|
|
|
// Setup database watcher (async, but don't wait - it will handle errors internally)
|
|
setupDatabaseWatcher().catch(err => {
|
|
sdk.log.error('global.profile', `Error in database watcher setup: ${err.message}`);
|
|
});
|
|
|
|
// Watch for peer connections/disconnections to update online status
|
|
peerConnectedHandler = async (data) => {
|
|
const { getProfileFromDB } = require('./database');
|
|
const profile = await getProfileFromDB(data.peerId);
|
|
if (profile) {
|
|
// Parse JSON fields for broadcast
|
|
try {
|
|
profile.customFields = profile.customFields ? JSON.parse(profile.customFields) : {};
|
|
profile.tags = profile.tags ? JSON.parse(profile.tags) : [];
|
|
} catch (err) {
|
|
profile.customFields = {};
|
|
profile.tags = [];
|
|
}
|
|
|
|
broadcastProfileUpdate({
|
|
type: 'profile-update',
|
|
profile: profile
|
|
});
|
|
}
|
|
|
|
// Trigger active replication update when peer connects (backup mechanism)
|
|
// The replication manager will also handle this, but this ensures updates happen
|
|
try {
|
|
// Give replication a moment to establish
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Check if database replication is active and trigger update
|
|
const dbReplication = sdk.db.replication;
|
|
if (dbReplication && dbReplication.isActive()) {
|
|
const core = sdk.db.getCore();
|
|
if (core && !core.closed) {
|
|
const oldLength = core.length;
|
|
await core.update({ wait: true });
|
|
const newLength = core.length;
|
|
if (newLength > oldLength) {
|
|
sdk.log.info('global.profile', `Database updated after peer ${data.peerId.slice(0, 16)} connection, length: ${oldLength} -> ${newLength}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Avatars are now in HyperDB, so they replicate automatically with the database
|
|
// No separate drive update needed
|
|
} catch (err) {
|
|
sdk.log.debug('global.profile', `Error triggering replication update after peer connection: ${err.message}`);
|
|
}
|
|
};
|
|
sdk.events.on('peer-connected', peerConnectedHandler);
|
|
|
|
peerDisconnectedHandler = async (data) => {
|
|
const { getProfileFromDB } = require('./database');
|
|
const profile = await getProfileFromDB(data.peerId);
|
|
if (profile) {
|
|
// Parse JSON fields for broadcast
|
|
try {
|
|
profile.customFields = profile.customFields ? JSON.parse(profile.customFields) : {};
|
|
profile.tags = profile.tags ? JSON.parse(profile.tags) : [];
|
|
} catch (err) {
|
|
profile.customFields = {};
|
|
profile.tags = [];
|
|
}
|
|
|
|
broadcastProfileUpdate({
|
|
type: 'profile-update',
|
|
profile: profile
|
|
});
|
|
}
|
|
};
|
|
sdk.events.on('peer-disconnected', peerDisconnectedHandler);
|
|
|
|
// Verify replication is enabled
|
|
try {
|
|
const dbReplication = sdk.db.replication;
|
|
if (dbReplication) {
|
|
const dbStatus = dbReplication.getStatus();
|
|
if (dbStatus && dbStatus.active) {
|
|
sdk.log.info('global.profile', `Database replication active (${dbStatus.peers} peers)`);
|
|
} else {
|
|
sdk.log.info('global.profile', 'Database replication will be enabled automatically');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
sdk.log.debug('global.profile', `Could not check database replication status: ${err.message}`);
|
|
}
|
|
|
|
// Replication is now handled automatically via global Corestore and replication managers
|
|
// Active updates are triggered both by replication managers and peer connection events
|
|
|
|
sdk.log.info('global.profile', 'Plugin initialized successfully');
|
|
} catch (err) {
|
|
sdk.log.error('global.profile', `Error during initialization: ${err.message}`);
|
|
if (err.stack) {
|
|
sdk.log.error('global.profile', `Stack trace: ${err.stack}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Plugin Shutdown Hook
|
|
*/
|
|
async function onShutdown() {
|
|
sdk.log.info('global.profile', 'Shutting down Global Profile plugin...');
|
|
|
|
try {
|
|
teardownWebSocketHandlers();
|
|
teardownDatabaseWatcher();
|
|
if (peerConnectedHandler) {
|
|
sdk.events.off('peer-connected', peerConnectedHandler);
|
|
peerConnectedHandler = null;
|
|
}
|
|
if (peerDisconnectedHandler) {
|
|
sdk.events.off('peer-disconnected', peerDisconnectedHandler);
|
|
peerDisconnectedHandler = null;
|
|
}
|
|
sdk.websocket.close();
|
|
|
|
sdk.log.info('global.profile', 'Plugin shutdown complete');
|
|
} catch (err) {
|
|
sdk.log.error('global.profile', `Error during shutdown: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Export the plugin interface
|
|
module.exports = {
|
|
handler,
|
|
onInit,
|
|
onShutdown
|
|
};
|
|
|