/** * ╔════════════════════════════════════════════════════════════╗ * ║ AutoSync — GoatBot Command & Event Syncer ║ * ║ Author : HR ID OY | Compatible: GoatBot v6+ ║ * ║ Syncs all local commands/events to GoatStore API ║ * ╚════════════════════════════════════════════════════════════╝ */ "use strict"; const fs = require("fs"); const path = require("path"); const axios = require("axios"); const CONFIG = { API_BASE: "https://goatstore-nu.vercel.app/api", CATEGORIES: ["economy", "fun", "moderation", "games", "utility", "ai"], UPLOAD_DELAY: 600, WATCH_DEBOUNCE: 3000, STARTUP_DELAY: 10000, }; const SYNC_CACHE_PATH = path.join(process.cwd(), "autosync_cache.json"); const DIR_CACHE_PATH = path.join(process.cwd(), "autosync_dircache.json"); let _watchersStarted = false; let _watchDebounce = null; let _syncInProgress = false; let _notifyThreadID = null; let _notifyApi = null; function loadJson(filePath, fallback = {}) { try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (_) { return fallback; } } function saveJson(filePath, data) { try { fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } catch (_) {} } function hashContent(content) { let h = 0; for (let i = 0; i < content.length; i++) h = (h * 31 + content.charCodeAt(i)) | 0; return h.toString(16); } function detectFramework(code) { return ( /module\.exports\s*=\s*\{/.test(code) && /onStart\s*[:(]|onChat\s*[:(]|onLoad\s*[:(]/.test(code) && /\bauthor\s*:/.test(code) ) ? "goat" : "other"; } const EVENTS_PATTERNS = ["events", "event"]; const SKIP_DIRS = new Set(["node_modules", ".git", ".cache", "dist", "build"]); let _dirCache = loadJson(DIR_CACHE_PATH, {}); function scanForDir(startDir, patterns, maxDepth = 2) { const queue = [{ dir: startDir, depth: 0 }]; while (queue.length) { const { dir, depth } = queue.shift(); let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { continue; } for (const ent of entries) { if (!ent.isDirectory() || SKIP_DIRS.has(ent.name) || ent.name.startsWith(".")) continue; const full = path.join(dir, ent.name); if (patterns.includes(ent.name.toLowerCase())) return full; if (depth < maxDepth) queue.push({ dir: full, depth: depth + 1 }); } } return null; } function getCmdsDir(forceRescan = false) { if (!forceRescan && _dirCache.cmdsDir && fs.existsSync(_dirCache.cmdsDir)) return _dirCache.cmdsDir; _dirCache.cmdsDir = __dirname; saveJson(DIR_CACHE_PATH, _dirCache); return __dirname; } function getEventsDir(forceRescan = false) { if (!forceRescan && _dirCache.eventsDir && fs.existsSync(_dirCache.eventsDir)) return _dirCache.eventsDir; const parent = path.dirname(getCmdsDir(forceRescan)); const dir = scanForDir(parent, EVENTS_PATTERNS, 2) || path.join(parent, "events"); _dirCache.eventsDir = dir; saveJson(DIR_CACHE_PATH, _dirCache); return dir; } async function apiUpload({ name, category, description, author, code, kind }) { const res = await axios.post( `${CONFIG.API_BASE}/commands`, { name, category, description, author, code, kind }, { headers: { "Content-Type": "application/json" } } ); return res.data; } async function runSync() { const folders = [ { dir: getCmdsDir(), kind: "command" }, { dir: getEventsDir(), kind: "event" }, ].filter(f => f.dir && fs.existsSync(f.dir)); const summary = { uploaded: 0, skipped: 0, errors: 0, results: [] }; if (!folders.length) { summary.results.push("⚠️ No commands/events folders found."); return summary; } const cache = loadJson(SYNC_CACHE_PATH, {}); for (const { dir, kind } of folders) { let files; try { files = fs.readdirSync(dir).filter(f => f.endsWith(".js")); } catch (_) { continue; } for (const file of files) { const fullPath = path.join(dir, file); let code; try { code = fs.readFileSync(fullPath, "utf8"); } catch (_) { summary.errors++; summary.results.push(`❌ ${file} — could not read file`); continue; } const hash = hashContent(code); const cacheKey = `${kind}:${file}`; if (cache[cacheKey]?.hash === hash) { summary.skipped++; continue; } try { new Function(code); } catch (syntaxErr) { summary.errors++; summary.results.push(`❌ ${file} — syntax error: ${syntaxErr.message}`); continue; } if (detectFramework(code) !== "goat") { summary.skipped++; continue; } const name = code.match(/name\s*:\s*["'`](.*?)["'`]/)?.[1] || path.basename(file, ".js"); const author = code.match(/author\s*:\s*["'`](.*?)["'`]/)?.[1] || "Unknown"; const description = code.match(/shortDescription\s*:\s*["'`](.*?)["'`]/)?.[1] || "No description"; const category = (code.match(/category\s*:\s*["'`](.*?)["'`]/)?.[1] || "utility").toLowerCase(); try { const result = await apiUpload({ name, category, description, author, code, kind }); if (!result.error) { const id = result.seq ? `#${String(result.seq).padStart(2, "0")}` : (result._id || result.id || "?"); cache[cacheKey] = { hash, id: result._id || result.id }; summary.uploaded++; summary.results.push(`✅ ${kind === "event" ? "⚡" : "🧩"} ${name} — synced (ID: ${id})`); } else { summary.errors++; summary.results.push(`❌ ${name} — API error: ${result.error}`); } } catch (err) { const e = err.response?.data?.error || err.message; summary.errors++; summary.results.push(`❌ ${file} — upload failed: ${e}`); } await new Promise(r => setTimeout(r, CONFIG.UPLOAD_DELAY)); } } saveJson(SYNC_CACHE_PATH, cache); return summary; } function buildSummaryMsg(summary, trigger = "manual") { const label = { manual: "📤 Manual Sync", startup: "🚀 Startup Sync", watcher: "👁️ File Watcher Sync" }[trigger] || "📤 Sync"; let msg = `╔══ ${label} ══╗\n`; msg += `╠ ✅ Uploaded : ${summary.uploaded}\n`; msg += `╠ ⏭️ Skipped : ${summary.skipped}\n`; msg += `╠ ❌ Errors : ${summary.errors}\n`; msg += `╠══════════════════╣\n`; const shown = summary.results.filter(r => !r.startsWith("⏭")); if (shown.length) { shown.forEach(r => { msg += `╠ ${r}\n`; }); msg += `╚══════════════════╝`; } else if (summary.uploaded === 0 && summary.errors === 0) { msg += `╠ 💤 Everything already up to date\n╚══════════════════╝`; } else { msg += `╚══════════════════╝`; } return msg; } async function doSync(trigger = "manual", api = null, threadID = null) { if (_syncInProgress) { if (api && threadID) api.sendMessage("⏳ Sync already in progress, please wait...", threadID); return; } _syncInProgress = true; try { const summary = await runSync(); const msg = buildSummaryMsg(summary, trigger); const notifyApi = api || _notifyApi; const notifyThreadID = threadID || _notifyThreadID; if (notifyApi && notifyThreadID) notifyApi.sendMessage(msg, notifyThreadID); else console.log("[autosync]", msg.replace(/\n/g, " | ")); } catch (err) { console.error("[autosync] unexpected error:", err.message); if (api && threadID) api.sendMessage(`❌ Sync crashed: ${err.message}`, threadID); } finally { _syncInProgress = false; } } function startWatcher() { if (_watchersStarted) return; _watchersStarted = true; const dirs = [getCmdsDir(), getEventsDir()].filter(d => d && fs.existsSync(d)); for (const dir of dirs) { try { fs.watch(dir, { persistent: false }, (eventType, filename) => { if (!filename || !filename.endsWith(".js")) return; clearTimeout(_watchDebounce); _watchDebounce = setTimeout(() => doSync("watcher").catch(() => {}), CONFIG.WATCH_DEBOUNCE); }); } catch (_) {} } } module.exports = { config: { name: "autosync", version: "1.0.0", author: "HR ID OY", countDown: 5, role: 0, shortDescription: "Auto-sync all commands & events to GoatStore", longDescription: "Automatically uploads all local GoatBot commands and events to GoatStore API. Syncs on startup, on file change, and on manual trigger.", category: "utility", guide: { en: "{pn}autosync — run a manual sync now\n{pn}autosync status — show cache info\n{pn}autosync reset — clear cache (force re-sync all next time)", }, }, onLoad: function () { startWatcher(); setTimeout(() => doSync("startup").catch(() => {}), CONFIG.STARTUP_DELAY); }, onStart: async function ({ api, event, args }) { const { threadID } = event; const sub = (args[0] || "").toLowerCase(); _notifyApi = api; _notifyThreadID = threadID; if (sub === "status") { const cache = loadJson(SYNC_CACHE_PATH, {}); const keys = Object.keys(cache); return api.sendMessage( `📊 AutoSync Status\n` + `━━━━━━━━━━━━━━━━\n` + `🧩 Commands cached : ${keys.filter(k => k.startsWith("command:")).length}\n` + `⚡ Events cached : ${keys.filter(k => k.startsWith("event:")).length}\n` + `📁 Total in cache : ${keys.length}\n` + `👁️ Watcher active : ${_watchersStarted ? "Yes" : "No"}\n` + `⏳ Sync in progress: ${_syncInProgress ? "Yes" : "No"}`, threadID ); } if (sub === "reset") { try { fs.unlinkSync(SYNC_CACHE_PATH); } catch (_) {} return api.sendMessage("🗑️ Cache cleared!\nNext sync will re-upload everything.", threadID); } await api.sendMessage("⏳ Starting sync...", threadID); await doSync("manual", api, threadID); }, };