const express = require('express'); const fs = require('fs').promises; const path = require('path'); // These are now passed by the loader, not required directly. // This makes the plugin more robust and independent. // const { ... } = require('../../../public/script.js'); const extensionName = "Amily-Databin"; const extensionVersion = "0.0.1-a"; let sourceTypes = {}; // Will be loaded from sourcetype.json /** * Main plugin initialization function. * SillyTavern's loader will call this function and provide context. * @param {object} context - The context provided by SillyTavern's loader. * @param {string} context.plugin_data_path - The base path for plugin data. * @param {object} context.plugin_name_to_folder_name - A map of plugin names to folder names. */ async function initializePlugin(context) { const { plugin_data_path, plugin_name_to_folder_name } = context; const storageBasePath = path.join(plugin_data_path, 'cc.amily49.amily-databin'); /** * Builds a safe file path and prevents path traversal attacks. */ function getSafeFilePath(source, objName, file) { if (typeof source !== 'string' && typeof source !== 'number' || typeof objName !== 'string' || typeof file !== 'string') { return null; } const sanitize = (part) => String(part).replace(/\.\./g, '').replace(/[/\]/g, ''); const safeSource = sanitize(source); const safeObjName = sanitize(objName); const safeFile = sanitize(file); if (!safeSource || !safeObjName || !safeFile) return null; return path.join(storageBasePath, safeSource, safeObjName, safeFile); } // #region Server-side Business Logic per SourceType async function defaultSaveStrategy(filePath, data) { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8'); } // #endregion try { // Load source types from the file on startup. const sourceTypesRaw = await fs.readFile(path.join(__dirname, 'sourcetype.json'), 'utf8'); sourceTypes = JSON.parse(sourceTypesRaw); await fs.mkdir(storageBasePath, { recursive: true }); } catch (error) { console.error(`[${extensionName}] Critical startup error:`, error); return null; // Return null to indicate failure } const router = express.Router(); router.get('/sourcetype', (req, res) => res.json(sourceTypes)); router.get('/data', async (req, res) => { const filePath = getSafeFilePath(req.query.source, req.query.objName, req.query.file); if (!filePath) return res.status(400).send("Invalid parameters."); try { const fileContent = await fs.readFile(filePath, 'utf8'); res.json(JSON.parse(fileContent)); } catch (error) { if (error.code === 'ENOENT') return res.status(404).send('Not found.'); res.status(500).send('Internal Server Error'); } }); router.post('/data', async (req, res) => { const { source, objName, file, data } = req.body; const filePath = getSafeFilePath(source, objName, file); if (!filePath || data === undefined) return res.status(400).send("Invalid or incomplete parameters."); const sourceType = parseInt(source, 10); try { switch (sourceType) { case sourceTypes.CHAT_LOG: case sourceTypes.CHARACTER_CARD: case sourceTypes.WORLD_BOOK: await defaultSaveStrategy(filePath, data); res.status(200).send('Data saved successfully.'); break; default: res.status(501).send(`Save handler for source type '${sourceType}' is not implemented.`); break; } } catch (error) { console.error(`[${extensionName}] Error in POST /data for source ${sourceType}:`, error); res.status(500).send('Internal Server Error'); } }); router.delete('/data', async (req, res) => { const filePath = getSafeFilePath(req.query.source, req.query.objName, req.query.file); if (!filePath) return res.status(400).send("Invalid parameters."); try { await fs.unlink(filePath); res.status(200).send('Data deleted successfully.'); } catch (error) { if (error.code === 'ENOENT') return res.status(200).send('Not found.'); res.status(500).send('Internal Server Error'); } }); // Return the plugin definition object return { name: extensionName, version: extensionVersion, is_enabled: true, folder_name: plugin_name_to_folder_name[extensionName], router: router, assets: ['client.js'], }; } // Export the main initialization function module.exports = initializePlugin;