const fs = require("fs"); const path = require("path"); const axios = require("axios"); // The bot is silent by default — nothing is printed to the host console // (where the bot runs) unless GOATSTORE_DEBUG is set. This keeps the host's // console clean while the store keeps working normally. Set // GOATSTORE_DEBUG=1 (or true/yes/on) to see sync/API diagnostics again. const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.GOATSTORE_DEBUG || "").trim()); function log(...args) { if (DEBUG) console.log(...args); } function logError(...args) { if (DEBUG) console.error(...args); } module.exports = { config: { name: "goatstore", aliases: ["gs", "cmdstore", "commandstore"], version: "20.3.0", author: "rX", countDown: 3, role: 2, shortDescription: "GoatBot Store — Search, AutoUpdate, Install, Upload, AutoSync", longDescription: "Browse, install, upload, and autosync GoatBot commands and events from the MiraiStore API. Auto-detects your cmds/events folder naming. The bare-menu shows only the daily-use commands — every subcommand lives here in the guide.", category: "system", guide: { en: "{pn} — Menu / Notifications\n" + "{pn} — Search commands only\n" + "{pn} -a — All files by an author\n" + "{pn} -c — Browse a framework or category\n" + "{pn} -e — Search events\n" + "{pn} -e install — Install as event\n" + "{pn} -e upload — Upload an event file\n" + "{pn} n — Today's updates\n" + "{pn} list [page] — Command list\n" + "{pn} list event [page] — Event list\n" + "{pn} install — Install a command\n" + "{pn} like — Like\n" + "{pn} trend — Trending\n" + "{pn} upload — Upload a command file\n" + "{pn} sync — Manual sync\n" + "{pn} dirs — Show & re-detect cmds/events locations\n" + "Reply \"in\" to a single result — Install\n" + "Reply \"in \" to a list result — Install" }, autoSync: true } }; // Store API base. The primary is the live store; the backup is only used on // a network error or a 5xx (a 4xx is a real answer — not-found, rate-limited, // blocked — and is never retried). STORE_API_BASE overrides the primary, for // local testing. const PRIMARY_BASE = (() => { const v = process.env.STORE_API_BASE; return v && v.startsWith("http") ? v.replace(/\/$/, "") : "https://store.agi.bd"; })(); const BACKUP_BASE = "https://rx.agi.bd"; function shouldFailover(err) { return !err.response || err.response.status >= 500; } // ── Outbound request governor ───────────────────────────────────────────── // The store blocks a caller that exceeds 20 requests in a rolling 10s window // (403 "Abnormal activity detected"), and a block also switches uploads off // for 3 hours. AutoSync alone can reach that ceiling: it posts one upload per // changed file, and the avatar / update-check loops run at the same time. // // So every request this file makes goes through ONE queue that guarantees: // • only one request is in flight at a time, // • consecutive requests are at least REQ_MIN_GAP_MS apart, and // • no more than REQ_MAX_PER_WINDOW are sent per REQ_WINDOW_MS. // The cap sits well under the server's limit — this paces the bot, it does // not weaken the store's abuse protection. const REQ_WINDOW_MS = 10 * 1000; const REQ_MAX_PER_WINDOW = 12; // store allows 20; keep a wide margin const REQ_MIN_GAP_MS = 150; // A stable caller id attached to every request. The store fingerprints a // caller by IP + clientFp and, for callers that send no clientFp, ALSO // latches an IP-level block. The bot shares one IP across every Messenger // user, so sending a clientFp keeps any block scoped to the bot and stops it // from taking real users down with it. const BOT_FP = `bot:${module.exports.config.author || "goatstore"}`; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const _reqTimes = []; let _reqChain = Promise.resolve(); function _schedule(makeReq) { const run = _reqChain.then(async () => { for (;;) { const now = Date.now(); while (_reqTimes.length && now - _reqTimes[0] >= REQ_WINDOW_MS) _reqTimes.shift(); if (_reqTimes.length >= REQ_MAX_PER_WINDOW) { await sleep(REQ_WINDOW_MS - (now - _reqTimes[0]) + 10); continue; } const last = _reqTimes[_reqTimes.length - 1] || 0; if (now - last < REQ_MIN_GAP_MS) { await sleep(REQ_MIN_GAP_MS - (now - last)); continue; } break; } _reqTimes.push(Date.now()); return makeReq(); }); // Keep the chain alive even when a request rejects. _reqChain = run.then(() => {}, () => {}); return run; } // How long the store says we're blocked for (seconds → ms). function blockedMs(err) { const s = Number(err?.response?.data?.remainingSeconds); return Number.isFinite(s) && s > 0 ? s * 1000 : 0; } // Queue one request; if the store blocks us, wait out the block instead of // hammering it (a couple of tries, then give up and surface the error). async function _send(makeReq, attempt = 0) { try { return await _schedule(makeReq); } catch (err) { const wait = blockedMs(err); if (wait > 0 && attempt < 2) { const ms = Math.min(wait, 5 * 60 * 1000) + 500; logError(`[goatstore] store rate limit hit — waiting ${Math.ceil(ms / 1000)}s before retrying`); await sleep(ms); return _send(makeReq, attempt + 1); } throw err; } } async function storeGet(pathSuffix, config = {}) { const cfg = { ...config, params: { ...(config.params || {}) } }; if (!cfg.params.clientFp && !/clientFp=/.test(pathSuffix)) cfg.params.clientFp = BOT_FP; try { return await _send(() => axios.get(`${PRIMARY_BASE}${pathSuffix}`, cfg)); } catch (err) { if (!shouldFailover(err)) throw err; logError(`[goatstore] ${PRIMARY_BASE} failed, retrying via ${BACKUP_BASE}`); return await _send(() => axios.get(`${BACKUP_BASE}${pathSuffix}`, cfg)); } } async function storePost(pathSuffix, data, config = {}) { const body = { ...(data || {}) }; if (!body.clientFp) body.clientFp = BOT_FP; try { return await _send(() => axios.post(`${PRIMARY_BASE}${pathSuffix}`, body, config)); } catch (err) { if (!shouldFailover(err)) throw err; logError(`[goatstore] ${PRIMARY_BASE} failed, retrying via ${BACKUP_BASE}`); return await _send(() => axios.post(`${BACKUP_BASE}${pathSuffix}`, body, config)); } } async function checkAbuseGuard(senderID) { try { const res = await storeGet(`/miraistore/ratelimit/status`, { params: { clientFp: senderID }, timeout: 6000 }); return res.data || { blocked: false }; } catch (_) { return { blocked: false }; } } function formatDuration(totalSeconds) { const h = Math.floor(totalSeconds / 3600); const m = Math.floor((totalSeconds % 3600) / 60); const s = totalSeconds % 60; return `${h}h ${m}m ${s}s`; } const userSeenNoti = new Map(); const AUTOSYNC_CACHE_PATH = path.join(process.cwd(), "goatstore_sync_cache.json"); const DIR_CACHE_PATH = path.join(process.cwd(), "goatstore_dircache.json"); let _updateCheckCache = null; const UPDATE_CHECK_INTERVAL = 1000 * 60 * 30; const MAX_EDITS_PER_MESSAGE = 5; function getPrefix(threadData) { try { if (threadData?.data?.prefix) return threadData.data.prefix; if (global.GoatBot?.config?.prefix) return global.GoatBot.config.prefix; } catch (_) {} return "!"; } function loadSyncCache() { try { return JSON.parse(fs.readFileSync(AUTOSYNC_CACHE_PATH, "utf8")); } catch { return {}; } } function saveSyncCache(cache) { try { fs.writeFileSync(AUTOSYNC_CACHE_PATH, JSON.stringify(cache, null, 2)); } catch (_) {} } let _autoupdateInFlight = false; async function stampUploadCode(code) { return `// code from goatstore\n// api: ${PRIMARY_BASE}\n${code}`; } 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 parseVer(v) { return String(v).split(".").map(n => parseInt(n) || 0); } function cmpVer(a, b) { const pa = parseVer(a), pb = parseVer(b); for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d !== 0) return d; } return 0; } function extractConfigBlock(src) { const idx = src.search(/\bconfig\s*[:=]\s*\{/); if (idx === -1) return src; const braceStart = src.indexOf("{", idx); if (braceStart === -1) return src; let depth = 0; for (let i = braceStart; i < src.length; i++) { if (src[i] === "{") depth++; else if (src[i] === "}") { depth--; if (depth === 0) return src.slice(braceStart, i + 1); } } return src.slice(braceStart); } function detectFramework(code) { const configBlock = extractConfigBlock(code); const hasCredits = /\bcredits\s*:/.test(configBlock); const hasPermission = /\bhasPerm(?:i)?ssion\s*[:(]/i.test(configBlock); if (hasCredits && hasPermission) return "mirai"; const hasAuthor = /\bauthor\s*:/.test(configBlock); const hasRole = /\brole\s*:/.test(configBlock); if (hasAuthor && hasRole) return "goat"; const isGoatStructure = /module\.exports\s*=\s*\{/.test(code) && /onStart\s*[:(]|onChat\s*[:(]|onLoad\s*[:(]/.test(code); if (isGoatStructure) return "goat"; const isMiraiStructure = /module\.exports\.config\s*=/.test(code) || /module\.exports\.run\s*=/.test(code); if (isMiraiStructure) return "mirai"; return "other"; } const EVENTS_NAME_PATTERNS = ["events", "event"]; const SCAN_SKIP_DIRS = new Set(["node_modules", ".git", ".cache", ".github", "dist", "build"]); function loadDirCache() { try { return JSON.parse(fs.readFileSync(DIR_CACHE_PATH, "utf8")); } catch { return {}; } } function saveDirCache(cache) { try { fs.writeFileSync(DIR_CACHE_PATH, JSON.stringify(cache, null, 2)); } catch (_) {} } let _dirCache = loadDirCache(); function scanForDir(startDir, namePatterns, 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()) continue; if (SCAN_SKIP_DIRS.has(ent.name)) continue; if (ent.name.startsWith(".")) continue; const lower = ent.name.toLowerCase(); const full = path.join(dir, ent.name); if (namePatterns.includes(lower)) 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; const dir = __dirname; _dirCache.cmdsDir = dir; saveDirCache(_dirCache); return dir; } function getEventsDir(forceRescan = false) { if (!forceRescan && _dirCache.eventsDir && fs.existsSync(_dirCache.eventsDir)) return _dirCache.eventsDir; const cmdsDir = getCmdsDir(forceRescan); const parent = path.dirname(cmdsDir); const roots = [...new Set([parent, path.dirname(parent), process.cwd()])]; let dir = null; for (const root of roots) { dir = scanForDir(root, EVENTS_NAME_PATTERNS, 3); if (dir) break; } if (!dir) dir = path.join(parent, "events"); _dirCache.eventsDir = dir; saveDirCache(_dirCache); return dir; } function findLocalFile(fileName, kind) { const dirs = kind === "event" ? [getEventsDir()] : [getCmdsDir(), getEventsDir()]; for (const dir of dirs) { const direct = path.join(dir, fileName); if (fs.existsSync(direct)) return { filePath: direct, dirs }; const withExt = direct.endsWith(".js") ? null : direct + ".js"; if (withExt && fs.existsSync(withExt)) return { filePath: withExt, dirs }; } return { filePath: null, dirs }; } function relDir(p) { const rel = path.relative(process.cwd(), p); return rel || "."; } function fileNotFoundMsg(fileName, dirs, prefix) { return ( `❌ File not found: "${fileName}"\nSearched in:\n` + dirs.map(d => `• ${relDir(d)}`).join("\n") + `\n💡 Wrong location? Check with: ${prefix}gs dirs` ); } async function checkSelfUpdate() { const now = Date.now(); if (_updateCheckCache && (now - _updateCheckCache.checkedAt) < UPDATE_CHECK_INTERVAL) return _updateCheckCache.result; try { const res = await storeGet(`/miraistore/search?q=goatstore&limit=10&framework=goat&kind=command`); const cmds = Array.isArray(res.data?.commands) ? res.data.commands : []; const match = cmds.find(c => c.name?.toLowerCase() === "goatstore" && c.author === module.exports.config.author) || cmds.find(c => c.name?.toLowerCase() === "goatstore"); if (!match) { _updateCheckCache = { checkedAt: now, result: null }; return null; } const current = module.exports.config.version; const latest = match.version || "N/A"; const result = { hasUpdate: cmpVer(latest, current) > 0, currentVersion: current, latestVersion: latest, latestId: match.id, description: match.description || match.changelog || "" }; _updateCheckCache = { checkedAt: now, result }; return result; } catch (_) { return null; } } async function getTodayUpdates(senderID = null) { try { const res = await storeGet(`/miraistore/list?limit=50&framework=goat&clientFp=${encodeURIComponent(senderID || "")}`); const today = new Date().toDateString(); return (res.data.commands || []) .filter(cmd => new Date(cmd.uploadDate).toDateString() === today) } catch (_) { return []; } } async function getTrending(limit = 5) { const parse = d => Array.isArray(d) ? d : (Array.isArray(d?.commands) ? d.commands : null); try { const res = await storeGet(`/miraistore/trending?limit=${limit}`); const list = parse(res.data); if (list) return list.slice(0, limit); } catch (_) {} return null; } // ── Developer avatar sync ────────────────────────────────────────────────────── // A bot that has goatstore.js installed publishes its OWN account profile // picture as its developer avatar. GoatBot's FCA API exposes the picture in // the getUserInfo() result (thumbSrc / profileUrl / imageSrc, depending on // FCA version), so we read it there and POST it to /miraistore/profile. The // GET first means a restart with an unchanged avatar doesn't re-send it. async function fetchBotAvatar(api) { let botId = null; try { botId = api.getCurrentUserID(); } catch (_) {} if (!botId && global.GoatBot?.botID) botId = global.GoatBot.botID; if (!botId) return null; let info = null; try { const res = await api.getUserInfo(botId); info = res && (res[botId] || res[String(botId)]) ? (res[botId] || res[String(botId)]) : res; } catch (_) { return null; } if (!info) return null; const url = info.thumbSrc || info.profileUrl || info.profilePictureUrl || info.imageSrc || info.image || info.avatar || (info.profilePicture && (info.profilePicture.uri || info.profilePicture.url)) || (info.thumbSrcBig) || null; return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null; } async function reportBotAvatar(api, { force = false } = {}) { const author = module.exports.config.author; if (!author) return; try { const avatarUrl = await fetchBotAvatar(api); if (!avatarUrl) return; if (!force) { try { const cur = await storeGet(`/miraistore/profile?author=${encodeURIComponent(author)}`); if (cur.data && cur.data.avatarUrl === avatarUrl) return; } catch (_) {} } await storePost(`/miraistore/profile`, { author, avatarUrl, clientFp: `avatar:${author}` }); log(`[goatstore] developer avatar reported for "${author}"`); } catch (err) { logError("[goatstore] avatar report failed:", err.response?.data?.error || err.message); } } async function runAutoSync() { const folders = [ { dir: getCmdsDir(), kind: "command" }, { dir: getEventsDir(), kind: "event" } ].filter(f => fs.existsSync(f.dir)); if (!folders.length) return; const cache = loadSyncCache(); // The store caps rewrites at 2 per bot per rolling minute. Sync at that // pace instead of hammering the API: after two rewrites, pause for the // rest of the minute before continuing. New entries aren't counted here // (the server only charges the rewrite path). let rewritesThisMinute = 0; const paceRewrites = async (isRewrite) => { if (!isRewrite) return; rewritesThisMinute++; if (rewritesThisMinute >= 2) { await new Promise(r => setTimeout(r, 60 * 1000)); rewritesThisMinute = 0; } }; for (const { dir, kind } of folders) { const files = fs.readdirSync(dir).filter(f => f.endsWith(".js")); for (const file of files) { const fullPath = path.join(dir, file); const cacheKey = `${kind}:${file}`; let content; try { content = fs.readFileSync(fullPath, "utf8"); } catch (_) { continue; } const hash = hashContent(content); if (cache[cacheKey] === hash) continue; try { new Function(content); } catch (_) { continue; } const fw = detectFramework(content); if (fw !== "goat") { log(`[goatstore-sync] Skipped ${file}: detected as "${fw}" (only GoatBot files are synced).`); continue; } try { const author = content.match(/author\s*:\s*["'`](.*?)["'`]/)?.[1] || content.match(/credits\s*:\s*["'`](.*?)["'`]/)?.[1] || "Unknown"; const category = content.match(/category\s*:\s*["'`](.*?)["'`]/)?.[1] || "Uncategorized"; const stampedContent = await stampUploadCode(content); const res = await storePost(`/miraistore/upload`, { rawCode: stampedContent, framework: "goat", kind, author, category }); if (res.data?.error) { logError(`[goatstore-sync] Upload skipped for ${file}: ${res.data.message || res.data.error}`); } else if (res.data?.updated) { log(`[goatstore-sync] ${file}: updated existing entry (ID: ${res.data.id}) to v${res.data.version}.`); cache[cacheKey] = hash; await paceRewrites(true); } else { log(`[goatstore-sync] ${file}: uploaded as new entry (ID: ${res.data.id}).`); cache[cacheKey] = hash; } } catch (err) { logError(`[goatstore-sync] Upload request fail for ${file}:`, err.response?.data?.error || err.message); } await new Promise(r => setTimeout(r, 500)); } } saveSyncCache(cache); } const buildBar = pct => "█".repeat(Math.floor(pct / 10)) + "░".repeat(10 - Math.floor(pct / 10)); const frames = ["◖", "◕", "◔", "◓", "◒", "◑", "◐"]; async function animateInstall(api, threadID, name) { const steps = [ { label: "Downloading source", pct: 30, delay: 600 }, { label: "Verifying integrity", pct: 60, delay: 900 }, { label: "Writing to disk", pct: 85, delay: 700 }, { label: "Registering command", pct: 100, delay: 600 } ]; const info = await api.sendMessage(`📦 Installing ${name}...\n\n◖ Fetching package info...\n[░░░░░░░░░░] 0%`, threadID); for (let i = 0; i < steps.length; i++) { await new Promise(r => setTimeout(r, steps[i].delay)); await api.editMessage(`📦 Installing ${name}...\n\n${frames[i]} ${steps[i].label}...\n[${buildBar(steps[i].pct)}] ${steps[i].pct}%`, info.messageID); } return info.messageID; } async function animateUpload(api, threadID, name) { const steps = [ { label: "Reading file", pct: 30, delay: 500 }, { label: "Uploading directly", pct: 70, delay: 900 }, { label: "Finalizing registration", pct: 100, delay: 500 } ]; const info = await api.sendMessage(`📤 Uploading ${name}...\n\n◖ Preparing upload...\n[░░░░░░░░░░] 0%`, threadID); for (let i = 0; i < steps.length; i++) { await new Promise(r => setTimeout(r, steps[i].delay)); await api.editMessage(`📤 Uploading ${name}...\n\n${frames[i]} ${steps[i].label}...\n[${buildBar(steps[i].pct)}] ${steps[i].pct}%`, info.messageID); } return info.messageID; } function autoloadCommand(filePath) { try { delete require.cache[require.resolve(filePath)]; const cmd = require(filePath); if (cmd?.config?.name) { const name = cmd.config.name.toLowerCase(); global.GoatBot.commands.set(name, cmd); if (Array.isArray(cmd.config.aliases)) cmd.config.aliases.forEach(a => global.GoatBot.commands.set(a.toLowerCase(), cmd)); if (typeof cmd.onLoad === "function") cmd.onLoad({}); return { success: true, name }; } return { success: false, reason: "Missing config.name." }; } catch (err) { return { success: false, reason: err.message }; } } // GoatBot dispatches BOTH onChat and onReply for the same "in"/"in " // message, so an install could run twice and show two progress messages. // This short-lived per-(thread,id) lock makes the second dispatch a no-op; // the TTL lets a genuine retry after a failure work again. const _installLocks = new Map(); async function doInstall(api, threadID, senderID, id, forceKind = null) { const key = `${threadID}:${id}`; const now = Date.now(); const prev = _installLocks.get(key); if (prev && now - prev < 8000) return; _installLocks.set(key, now); try { return await installCommand(api, threadID, senderID, id, forceKind); } finally { setTimeout(() => { if (_installLocks.get(key) === now) _installLocks.delete(key); }, 8000).unref?.(); } } async function installCommand(api, threadID, senderID, id, forceKind = null) { let cmdData = null; try { const res = await storeGet(`/miraistore/search?q=${encodeURIComponent(id)}&clientFp=${encodeURIComponent(senderID || "")}`); const data = res.data; if (!isNaN(id) && data?.rawCode && !Array.isArray(data)) cmdData = data; else if (Array.isArray(data?.commands)) cmdData = data.commands.find(c => String(c.id) === String(id)); if (!cmdData?.rawCode) return api.sendMessage("❌ Command not found or rawCode missing.", threadID); } catch (err) { return api.sendMessage(err.response?.data?.message || "❌ Failed to fetch command info.", threadID); } if (cmdData.framework !== "goat") return api.sendMessage( `❌ This is not a GoatBot file!\n` + `├‣ Category : ${cmdData.framework || "unknown"}\n` + `╰────────────◊\n` + `⚠️ Only goat-framework commands/events can be installed here.`, threadID ); try { new Function(cmdData.rawCode); } catch (err) { return api.sendMessage(`❌ Syntax error in remote code.\n${err.message}`, threadID); } const displayName = cmdData.name || `gs_${id}`; const isEvent = forceKind === "event" ? true : forceKind === "command" ? false : cmdData.kind === "event"; let pid; try { pid = await animateInstall(api, threadID, displayName); } catch (_) {} const fileName = displayName.replace(/\s+/g, "_") + ".js"; const baseDir = process.cwd(); const installDir = isEvent ? getEventsDir() : getCmdsDir(); const filePath = path.join(installDir, fileName); const locLabel = path.relative(baseDir, filePath); try { if (!fs.existsSync(installDir)) fs.mkdirSync(installDir, { recursive: true }); fs.writeFileSync(filePath, cmdData.rawCode, "utf-8"); } catch (err) { if (pid) api.unsendMessage(pid); return api.sendMessage(`❌ Failed to write file:\n${err.message}`, threadID); } try { await storePost(`/miraistore/install/${cmdData.id}`, { clientFp: senderID }); } catch (_) {} const load = isEvent ? { success: false } : autoloadCommand(filePath); const msg = `✅ Installed Successfully!\n` + `╭─‣ Name : ${cmdData.name || "Unknown"}\n` + `├‣ Type : ${typeBadge(cmdData)}\n` + `├‣ Author : ${cmdData.author || "Unknown"}\n` + `├‣ Version : ${cmdData.version || "N/A"}\n` + `├‣ Category : ${cmdData.category || "N/A"}\n` + `├‣ ID : ${id}\n` + `├‣ Location : ${locLabel}\n` + `╰────────────◊\n` + (load.success ? `🚀 "${load.name}" is now live! No restart needed.` : isEvent ? `⚠️ Event saved. Restart bot to apply.` : `⚠️ Autoload failed: ${load.reason}`); if (pid) { try { await api.editMessage(msg, pid); setTimeout(() => api.unsendMessage(pid).catch(() => {}), 5000); } catch (_) { api.sendMessage(msg, threadID); } } else api.sendMessage(msg, threadID); } async function doSelfUpdateSilent(api, threadID, selfUpdate) { let cmdData = null; try { const res = await storeGet(`/miraistore/search?q=${encodeURIComponent(selfUpdate.latestId)}`); const data = res.data; if (!isNaN(selfUpdate.latestId) && data?.rawCode && !Array.isArray(data)) cmdData = data; else if (Array.isArray(data?.commands)) cmdData = data.commands.find(c => String(c.id) === String(selfUpdate.latestId)); if (!cmdData?.rawCode) return false; } catch (_) { return false; } try { new Function(cmdData.rawCode); } catch (_) { return false; } try { fs.writeFileSync(__filename, cmdData.rawCode, "utf-8"); } catch (_) { return false; } try { await storePost(`/miraistore/install/${cmdData.id}`, { }); } catch (_) {} const changelog = (cmdData.description || cmdData.changelog || "No changelog provided.").trim(); const load = autoloadCommand(__filename); if (api && threadID) { const msg = `♻️ Auto-Updated GoatStore!\n` + `╭─‣ Version : v${cmdData.version || selfUpdate.latestVersion}\n` + `├‣ ID : ${cmdData.id}\n` + `╰────────────◊\n` + `📝 Changelog:\n${changelog}\n\n` + (load.success ? `🚀 Live now! No restart needed.` : `⚠️ Reload failed (${load.reason}) — restart bot to apply.`); api.sendMessage(msg, threadID).catch(() => {}); } return true; } async function maybeAutoUpdate(api, threadID) { if (_autoupdateInFlight) return; const selfUpdate = await checkSelfUpdate(); if (!selfUpdate?.hasUpdate) return; _autoupdateInFlight = true; try { await doSelfUpdateSilent(api, threadID, selfUpdate); } finally { _autoupdateInFlight = false; } } function typeBadge(cmd) { if (cmd.framework === "goat") return cmd.kind === "event" ? "🐐 G-Event" : "🐐 G-Bot"; if (cmd.framework === "mirai") return cmd.kind === "event" ? "🌌 Mirai-E" : "🌌 Mirai"; return "📦 Other"; } function authorLine(cmd) { const v = cmd.version && cmd.version !== "N/A" ? ` (v${cmd.version})` : ""; return `${cmd.author || "Unknown"}${v}`; } function resultBlock(cmd) { const nameLine = cmd.name; return ( `╭─‣ ${nameLine} 〄\n` + `├‣ ID : ${cmd.id}\n` + `├‣ Type : ${typeBadge(cmd)}\n` + `├‣ Author : ${cmd.author || "Unknown"}\n` + `├‣ Version : ${cmd.version && cmd.version !== "N/A" ? ` ${cmd.version}` : " N/A"}\n` + `├‣ Category : ${cmd.category}\n` + `╰────────────◊\n` + ` ✰ Upload : ${new Date(cmd.uploadDate || Date.now()).toDateString()}\n\n` ); } function listBlock(cmd) { const nameLine = cmd.name; return ( `╭─‣ ${nameLine} 〄\n` + `├‣ ID : ${cmd.id}\n` + `├‣ Author : ${cmd.author || "Unknown"}\n` + `├‣ Version : ${cmd.version && cmd.version !== "N/A" ? ` ${cmd.version}` : " N/A"}\n` + `├‣ Category : ${cmd.category}\n` + `╰────────────◊\n` + ` ✰ Upload : ${new Date(cmd.uploadDate || Date.now()).toDateString()}\n\n` ); } async function sendListPage(api, threadID, senderID, kind, page, limit = 10, prefix = "!") { const offset = (page - 1) * limit; try { const res = await storeGet(`/miraistore/list?limit=${limit}&offset=${offset}&framework=goat&kind=${kind}&clientFp=${encodeURIComponent(senderID || "")}`); const data = res.data; if (!Array.isArray(data.commands) || !data.commands.length) return api.sendMessage("❌ No results found for this page.", threadID); const totalPages = Math.ceil(data.total / limit); const label = kind === "event" ? "GoatBot Events" : "GoatBot Commands"; let msg = `📂 ${label} — Page ${page}/${totalPages} (${data.total} total)\n\n`; data.commands.forEach(cmd => { msg += listBlock(cmd); }); if (totalPages > 1) msg += `⏤͟͟͞͞ Page ${page}/${totalPages}\n╭‣ React or reply p ${page + 1 <= totalPages ? page + 1 : page} for nxt pg\n`; msg += `╰‣ reply in for install`; const finalMsg = msg.trim(); const sent = await api.sendMessage(finalMsg, threadID); { const h = { commandName: "goatstore", messageID: sent.messageID, listType: kind, page, totalPages, limit, mode: "list", senderID, editCount: 0 }; global.GoatBot.onReply.set(sent.messageID, h); global.GoatBot.onReaction.set(sent.messageID, h); } } catch (_) { api.sendMessage("❌ List API error.", threadID); } } function searchTitle(query, filterOpts) { if (filterOpts.author) return `👤 Author: ${filterOpts.author}`; if (filterOpts.category && !query) return `📂 Category: ${filterOpts.category}`; if (filterOpts.framework && !query) return `📂 Category: ${filterOpts.framework}`; if (filterOpts.kind === "event" && !query) return `📂 Events`; return `🔍 Search: "${query}"`; } async function sendSearchPage(api, threadID, senderID, query, page, limit = 5, prefix = "!", filterOpts = {}) { const offset = (page - 1) * limit; try { let qs = `/miraistore/search?limit=${limit}&offset=${offset}`; if (filterOpts.author) qs += `&author=${encodeURIComponent(filterOpts.author)}`; else qs += `&q=${encodeURIComponent(query || "")}`; if (filterOpts.framework) qs += `&framework=${filterOpts.framework}`; if (filterOpts.kind) qs += `&kind=${filterOpts.kind}`; if (filterOpts.category) qs += `&category=${encodeURIComponent(filterOpts.category)}`; qs += `&clientFp=${encodeURIComponent(senderID || "")}`; const res = await storeGet(qs); const data = res.data; if (!Array.isArray(data.commands) || !data.commands.length) return api.sendMessage(`❌ No results found${query ? ` for "${query}"` : ""}.`, threadID); const total = data.total || data.commands.length; const totalPages = Math.max(1, Math.ceil(total / limit)); const title = searchTitle(query, filterOpts); let msg = `${title} (${total} found)\n\n`; data.commands.forEach(cmd => { msg += resultBlock(cmd); }); if (totalPages > 1) msg += `⏤͟͟͞͞ Page ${page}/${totalPages}\n╭‣ React or reply p ${page + 1 <= totalPages ? page + 1 : page} for nxt pg\n`; msg += `╰‣ reply in for install`; const finalMsg = msg.trim(); const sent = await api.sendMessage(finalMsg, threadID); const h = { commandName: "goatstore", messageID: sent.messageID, query, authorQuery: filterOpts.author || null, framework: filterOpts.framework || null, kind: filterOpts.kind || null, category: filterOpts.category || null, page, totalPages, limit, mode: "search", senderID, editCount: 0 }; global.GoatBot.onReply.set(sent.messageID, h); if (totalPages > 1) global.GoatBot.onReaction.set(sent.messageID, h); } catch (err) { api.sendMessage(err.response?.data?.message || "❌ Search API error.", threadID); } } async function renderListPageInto(messageID, kind, page, limit, senderID = null) { const offset = (page - 1) * limit; const res = await storeGet(`/miraistore/list?limit=${limit}&offset=${offset}&framework=goat&kind=${kind}&clientFp=${encodeURIComponent(senderID || "")}`); const data = res.data; if (!Array.isArray(data.commands) || !data.commands.length) return null; const totalPages = Math.ceil(data.total / limit); const label = kind === "event" ? "GoatBot Events" : "GoatBot Commands"; let msg = `📂 ${label} — Page ${page}/${totalPages} (${data.total} total)\n\n`; data.commands.forEach(cmd => { msg += listBlock(cmd); }); if (totalPages > 1) msg += `⏤͟͟͞͞ Page ${page}/${totalPages}\n╭‣ React or reply p ${page + 1 <= totalPages ? page + 1 : page} for nxt pg\n`; msg += `╰‣ reply in for install`; return { text: msg.trim(), totalPages }; } async function renderSearchPageInto(query, page, limit, filterOpts = {}, senderID = null) { const offset = (page - 1) * limit; let qs = `/miraistore/search?limit=${limit}&offset=${offset}`; if (filterOpts.author) qs += `&author=${encodeURIComponent(filterOpts.author)}`; else qs += `&q=${encodeURIComponent(query || "")}`; if (filterOpts.framework) qs += `&framework=${filterOpts.framework}`; if (filterOpts.kind) qs += `&kind=${filterOpts.kind}`; if (filterOpts.category) qs += `&category=${encodeURIComponent(filterOpts.category)}`; qs += `&clientFp=${encodeURIComponent(senderID || "")}`; const res = await storeGet(qs); const data = res.data; if (!Array.isArray(data.commands) || !data.commands.length) return null; const total = data.total || data.commands.length; const totalPages = Math.max(1, Math.ceil(total / limit)); const title = searchTitle(query, filterOpts); let msg = `${title} (${total} found)\n\n`; data.commands.forEach(cmd => { msg += resultBlock(cmd); }); if (totalPages > 1) msg += `⏤͟͟͞͞ Page ${page}/${totalPages}\n╭‣ React or reply p ${page + 1 <= totalPages ? page + 1 : page} for nxt pg\n`; msg += `╰‣ reply in for install`; return { text: msg.trim(), totalPages }; } async function uploadFile(api, threadID, filePath, kind, senderID = null) { let data; try { data = fs.readFileSync(filePath, "utf8"); } catch (err) { return api.sendMessage(`❌ Read failed:\n${err.message}`, threadID); } try { new Function(data); } catch (err) { return api.sendMessage(`❌ Syntax Error:\n${err.message}`, threadID); } const displayName = data.match(/name\s*:\s*["'`](.*?)["'`]/)?.[1] || path.basename(filePath); const detected = detectFramework(data); if (detected !== "goat") return api.sendMessage( `❌ Only GoatBot files can be uploaded here.\n` + `├‣ Detected : "${detected}" (this looks like a ${detected === "mirai" ? "Mirai" : "plain script"} file)\n` + `╰────────────◊`, threadID ); let pid; try { pid = await animateUpload(api, threadID, displayName); } catch (_) {} try { const stampedData = await stampUploadCode(data); const body = { rawCode: stampedData, framework: "goat", kind }; if (senderID) { body.uploaderID = senderID; body.clientFp = senderID; } const res = await storePost(`/miraistore/upload`, body); if (["Already exists", "Version already exists", "Version too low", "Not allowed", "Upload blocked"].includes(res.data?.error)) { if (pid) api.unsendMessage(pid); return api.sendMessage( `⚠️ Upload Blocked!\n` + `╭─‣ Name : ${displayName}\n` + (res.data.id ? `├‣ ID : ${res.data.id}\n` : "") + (res.data.currentVersion ? `├‣ Current : v${res.data.currentVersion}\n` : "") + `╰────────────◊\n` + `💡 ${res.data.message}`, threadID ); } if (res.data?.error) { if (pid) api.unsendMessage(pid); return api.sendMessage( `⚠️ Upload Failed!\n` + `╭─‣ Name : ${displayName}\n` + `├‣ Error : ${res.data.error}\n` + `╰────────────◊\n` + `💡 ${res.data.message || "MiraiStore backend register korte parenai. Backend/API side check koro."}`, threadID ); } const author = data.match(/author\s*:\s*["'`](.*?)["'`]/)?.[1] || data.match(/credits\s*:\s*["'`](.*?)["'`]/)?.[1] || "Unknown"; const version = data.match(/version\s*:\s*["'`](.*?)["'`]/)?.[1] || "N/A"; const category = data.match(/category\s*:\s*["'`](.*?)["'`]/)?.[1] || "Uncategorized"; let header = "✅ Upload Successful!"; let note = ""; if (res.data.olderVersion) { header = "⚠️ Older Version — Stored As New Entry!"; note = `💡 ${res.data.message}\n`; } else if (res.data.updated) { header = "🔄 Updated Existing Entry (Overwritten)!"; note = `💡 ${res.data.message}\n`; } const msg = `${header}\n` + `╭─‣ Name : ${displayName}\n` + `├‣ Type : ${res.data.type || `goat-${kind}`}\n` + `├‣ Version : ${version}\n` + `├‣ Author : ${author}\n` + `├‣ Category : ${category}\n` + `├‣ ID : ${res.data.id}\n` + `╰────────────◊\n` + note + `⭔ Upload : ${new Date().toDateString()}`; if (pid) { try { await api.editMessage(msg, pid); } catch (_) { api.sendMessage(msg, threadID); } } else api.sendMessage(msg, threadID); } catch (err) { if (pid) api.unsendMessage(pid); api.sendMessage( `⚠️ Store API Call Fail Korlo!\n` + `├‣ Error : ${err.response?.data?.error || err.message}\n` + `╰────────────◊\n` + `💡 ${err.response?.data?.message || "Request fail hoyeche, MiraiStore backend / network check koro."}`, threadID ); } } Object.assign(module.exports, { onLoad: function ({ api } = {}) { // Report the bot's own profile picture as the developer avatar, then // refresh it daily so a changed bot avatar propagates to the website. // Jittered start times so a fleet of bots doesn't hit the store on the // same tick, and so the three loops don't collide with each other. const jitter = (base, spread) => base + Math.floor(Math.random() * spread); const apiRef = api || global.GoatBot?.fcaApi; if (apiRef) { setTimeout(() => { reportBotAvatar(apiRef).catch(() => {}); }, jitter(4000, 6000)); setInterval(() => { reportBotAvatar(apiRef).catch(() => {}); }, 1000 * 60 * 60 * 24); } setTimeout(() => { maybeAutoUpdate(null, null).catch(() => {}); setInterval(() => { maybeAutoUpdate(null, null).catch(() => {}); }, UPDATE_CHECK_INTERVAL); }, jitter(6000, 10000)); if (module.exports.config.autoSync) { const ONE_DAY = 1000 * 60 * 60 * 24; setTimeout(() => { runAutoSync().catch(() => {}); setInterval(() => { runAutoSync().catch(() => {}); }, ONE_DAY); }, jitter(9000, 30000)); } }, onReply: async function ({ api, event, Reply }) { const { threadID, body, senderID } = event; const inIdMatch = body.match(/^in\s+(\d+)$/i); const inBareMatch = /^in$/i.test(body.trim()); if (inIdMatch) return doInstall(api, threadID, senderID, inIdMatch[1], null); if (inBareMatch && Reply?.singleId) return doInstall(api, threadID, senderID, Reply.singleId, null); const delMatch = body.match(/^(?:rmv|delete|remove)\s+(\S+)(?:\s+(\S+))?/i); if (delMatch) { const [, delId, delSecret] = delMatch; try { const payload = delSecret ? { secret: delSecret, userID: senderID } : { userID: senderID }; const res = await storePost(`/miraistore/delete/${delId}`, payload); if (res.data?.error) return api.sendMessage(`❌ ${res.data.error}`, threadID); return api.sendMessage(`🗑️ Deleted! ID: ${delId}`, threadID); } catch (_) { return api.sendMessage("❌ Delete API error.", threadID); } } const { mode, query, listType, authorQuery, framework, kind, category, page, totalPages, limit, senderID: origSender } = Reply; if (senderID !== origSender) return; const match = body.match(/^page (\d+)$/i); if (!match) return; const newPage = parseInt(match[1]); if (newPage < 1 || newPage > totalPages) return api.sendMessage(`❌ Page must be between 1 and ${totalPages}.`, threadID); api.unsendMessage(Reply.messageID).catch(() => {}); const prefix = getPrefix(event.threadData); if (mode === "list") await sendListPage(api, threadID, senderID, listType, newPage, limit, prefix); else await sendSearchPage(api, threadID, senderID, query, newPage, limit, prefix, { author: authorQuery, framework, kind, category }); }, onChat: async function ({ api, event }) { const { threadID, senderID, body, messageReply } = event; if (!body || !messageReply) return; const text = body.trim(); const inMatch = text.match(/^in\s+(\d+)$/i); const inBareMatch = /^in$/i.test(text); const rmvMatch = text.match(/^(?:rmv|remove)\s+(\d+)(?:\s+(\S+))?$/i); if (!inMatch && !inBareMatch && !rmvMatch) return; let isBotMsg = false; try { isBotMsg = String(messageReply.senderID) === String(api.getCurrentUserID()); } catch (_) {} if (!isBotMsg) return; const repliedBody = messageReply.body || ""; if (!/〄|🔍|📂|MiraiStore|GoatBot Store/i.test(repliedBody)) return; if (inMatch) return doInstall(api, threadID, senderID, inMatch[1], null); // Bare "in" — install the single result this message represents. onReply // stores the pending id on the bot's own message; onChat is dispatched // for the same reply, so both paths resolve the id the same way (and the // install lock collapses the duplicate). if (inBareMatch) { const pending = global.GoatBot.onReply.get(messageReply.messageID); if (pending?.singleId) return doInstall(api, threadID, senderID, pending.singleId, null); return; } const [, id, secret] = rmvMatch; try { const payload = secret ? { secret, userID: senderID } : { userID: senderID }; const res = await storePost(`/miraistore/delete/${id}`, payload); if (res.data?.error) return api.sendMessage(`❌ ${res.data.error}`, threadID); return api.sendMessage(`🗑️ Deleted! ID: ${id}`, threadID); } catch (_) { return api.sendMessage("❌ Delete API error.", threadID); } }, onReaction: async function ({ api, event, Reaction }) { const { threadID, userID } = event; const { mode, query, listType, authorQuery, framework, kind, category, page, totalPages, limit, senderID, messageID, editCount = 0 } = Reaction; if (userID !== senderID) return; if (page >= totalPages) return api.sendMessage("✅ Already on the last page.", threadID); const nextPage = page + 1; try { const rendered = mode === "list" ? await renderListPageInto(messageID, listType, nextPage, limit, senderID) : await renderSearchPageInto(query, nextPage, limit, { author: authorQuery, framework, kind, category }, senderID); if (!rendered) return api.sendMessage("❌ No results found for this page.", threadID); if (editCount >= MAX_EDITS_PER_MESSAGE) { const sent = await api.sendMessage(rendered.text, threadID); const h = { commandName: "goatstore", messageID: sent.messageID, listType, query, authorQuery, framework, kind, category, page: nextPage, totalPages: rendered.totalPages, limit, mode, senderID, editCount: 0 }; global.GoatBot.onReply.set(sent.messageID, h); global.GoatBot.onReaction.set(sent.messageID, h); } else { await api.editMessage(rendered.text, messageID); const h = { commandName: "goatstore", messageID, listType, query, authorQuery, framework, kind, category, page: nextPage, totalPages: rendered.totalPages, limit, mode, senderID, editCount: editCount + 1 }; global.GoatBot.onReply.set(messageID, h); global.GoatBot.onReaction.set(messageID, h); } } catch (_) { api.unsendMessage(messageID).catch(() => {}); const prefix = getPrefix(event.threadData); if (mode === "list") await sendListPage(api, threadID, senderID, listType, nextPage, limit, prefix); else await sendSearchPage(api, threadID, senderID, query, nextPage, limit, prefix, { author: authorQuery, framework, kind, category }); } }, onStart: async function ({ api, event, args, threadData }) { const { threadID, senderID } = event; const guard = await checkAbuseGuard(senderID); if (guard.blocked) { return api.sendMessage( `⚠️ Abnormal activity detected on your account.\n` + `GoatStore access is suspended for ${formatDuration(guard.remainingSeconds || 0)}.`, threadID ); } const sub = args[0]?.toLowerCase() || null; const prefix = getPrefix(threadData || event?.threadData); maybeAutoUpdate(api, threadID).catch(() => {}); if (!sub) { const updates = await getTodayUpdates(senderID); if (updates.length && !userSeenNoti.get(senderID)) { let n = `🔔 [ NOTIFICATION ]\nToday ${updates.length} GoatBot update(s)!\n━━━━━━━━━━━━━━━━━━\n`; updates.forEach(f => { n += ` ‣ ${f.name} (ID: ${f.id})\n`; }); n += `\n(Type "${prefix}gs n" for details or "${prefix}gs" again for menu)`; userSeenNoti.set(senderID, true); return api.sendMessage(n, threadID); } const menuMsg = `📦 GoatStore\n\nUsage:\n` + `• ${prefix}gs \n` + `• ${prefix}gs install \n` + `• ${prefix}gs upload \n` + `• ${prefix}gs trend — Trending\n` + `• ${prefix}gs sync — Sync your files`; await api.sendMessage(menuMsg, threadID); return; } if (sub === "n" || sub === "notification") { const updates = await getTodayUpdates(senderID); if (!updates.length) return api.sendMessage("📅 No GoatBot updates today.", threadID); let msg = `📂 Today's GoatBot Updates\n━━━━━━━━━━━━━━━━━━\n`; updates.forEach(cmd => { msg += `╭─‣ ${cmd.name}\n├‣ ID: ${cmd.id}\n├‣ Type: ${typeBadge(cmd)}\n├‣ Author: ${cmd.author}\n╰────────────◊\n\n`; }); await api.sendMessage(msg.trim(), threadID); return; } if (sub === "sync") { api.sendMessage("🔄 Starting manual sync...", threadID); try { await runAutoSync(); api.sendMessage("✅ Sync complete.", threadID); } catch (err) { api.sendMessage(`❌ Sync failed: ${err.message}`, threadID); } return; } if (sub === "dirs") { const cmdsDir = getCmdsDir(true); const eventsDir = getEventsDir(true); const countJs = d => { try { return fs.readdirSync(d).filter(f => f.endsWith(".js")).length; } catch { return null; } }; const cc = countJs(cmdsDir), ec = countJs(eventsDir); const msg = `📁 Auto-detected Locations\n` + `╭─‣ Commands : ${relDir(cmdsDir)}${cc !== null ? ` (${cc} .js files)` : " — not found"}\n` + `├‣ Events : ${relDir(eventsDir)}${ec !== null ? ` (${ec} .js files)` : " — not found"}\n` + `╰────────────◊\n` + `♻️ Auto-detect re-ran • AutoSync: ${module.exports.config.autoSync ? "ON ✅" : "OFF ❌"}`; return api.sendMessage(msg, threadID); } if (sub === "list" || sub === "ls") { const isEvent = args[1]?.toLowerCase() === "event"; const page = Math.max(1, Number(isEvent ? args[2] : args[1]) || 1); return sendListPage(api, threadID, senderID, isEvent ? "event" : "command", page, 10, prefix); } if (sub === "-e" || sub === "--event" || sub === "event") { const action = args[1]?.toLowerCase(); if (action === "install") { const id = args[2]; if (!id) return api.sendMessage(`❌ Usage: ${prefix}gs -e install `, threadID); return doInstall(api, threadID, senderID, id, "event"); } if (action === "upload") { const fileName = args[2]; if (!fileName) return api.sendMessage(`❌ Usage: ${prefix}gs -e upload `, threadID); const { filePath, dirs } = findLocalFile(fileName, "event"); if (!filePath) return api.sendMessage(fileNotFoundMsg(fileName, dirs, prefix), threadID); return uploadFile(api, threadID, filePath, "event", senderID); } if (!action) { try { const res = await storeGet(`/miraistore/list?limit=20&framework=goat&kind=event&clientFp=${encodeURIComponent(senderID || "")}`); const events = res.data.commands || []; if (!events.length) return api.sendMessage("❌ No GoatBot events found in store.", threadID); let msg = `📂 GoatBot Store Events (${res.data.total})\n\n`; events.forEach(cmd => { msg += `╭─‣ ${cmd.name}\n├‣ ID : ${cmd.id}\n├‣ Author : ${authorLine(cmd)}\n╰────────────◊\n\n`; }); msg += `💡 Use: ${prefix}gs -e install `; await api.sendMessage(msg.trim(), threadID); return; } catch (_) { return api.sendMessage("❌ Event list API error.", threadID); } } try { const res = await storeGet(`/miraistore/search?q=${encodeURIComponent(action)}&limit=5&framework=goat&kind=event&clientFp=${encodeURIComponent(senderID || "")}`); const events = res.data.commands || []; if (!events.length) return api.sendMessage(`❌ No GoatBot event found: "${action}"`, threadID); let msg = `📂 GoatBot Events matching "${action}"\n\n`; events.forEach(cmd => { msg += `╭─‣ ${cmd.name}\n├‣ ID : ${cmd.id}\n├‣ Author : ${authorLine(cmd)}\n╰────────────◊\n\n`; }); msg += `💡 Use: ${prefix}gs -e install `; await api.sendMessage(msg.trim(), threadID); return; } catch (_) { return api.sendMessage("❌ Event search API error.", threadID); } } if (sub === "install") { const id = args[1]; if (!id) return api.sendMessage(`❌ Usage: ${prefix}gs install `, threadID); return doInstall(api, threadID, senderID, id, null); } if (sub === "like") { const id = args[1]; if (!id) return api.sendMessage(`❌ Usage: ${prefix}gs like `, threadID); try { const res = await storePost(`/miraistore/like/${id}`, { userID: senderID, clientFp: senderID }); if (res.data?.message) return api.sendMessage("⚠️ Already liked.", threadID); return api.sendMessage(`❤️ Liked! Total Likes: ${res.data.likes}`, threadID); } catch (_) { return api.sendMessage("❌ Like API error.", threadID); } } if (sub === "trend" || sub === "trending") { const list = await getTrending(5); try { if (!list) return api.sendMessage("❌ Trending API error.", threadID); if (!list.length) return api.sendMessage("❌ No trending files.", threadID); let msg = `🔥 Top Trending 🔥\n\n`; list.forEach((cmd, i) => { msg += `╭─‣ ${cmd.name}${i === 0 ? " 🏆" : ""}\n` + `├‣ Type : ${typeBadge(cmd)}\n` + `├‣ Likes : ❤️ ${cmd.likes}\n` + `├‣ Views : 👁️ ${cmd.views}\n` + `├‣ ID : ${cmd.id}\n` + `╰────────────◊\n\n`; }); await api.sendMessage(msg.trim(), threadID); return; } catch (_) { return api.sendMessage("❌ Trending API error.", threadID); } } if (sub === "upload") { const isEvent = args[1]?.toLowerCase() === "event"; const fileName = isEvent ? args[2] : args[1]; const kind = isEvent ? "event" : "command"; if (!fileName) return api.sendMessage(`📁 Usage:\n• ${prefix}gs upload \n• ${prefix}gs -e upload (event)`, threadID); const { filePath, dirs } = findLocalFile(fileName, kind); if (!filePath) return api.sendMessage(fileNotFoundMsg(fileName, dirs, prefix), threadID); return uploadFile(api, threadID, filePath, kind, senderID); } if (sub === "delete") { const id = args[1], secret = args[2]; if (!id) return api.sendMessage(`❌ Usage: ${prefix}gs delete [secret]`, threadID); try { const payload = secret ? { secret, userID: senderID } : { userID: senderID }; const res = await storePost(`/miraistore/delete/${id}`, payload); if (res.data?.error) return api.sendMessage(`❌ ${res.data.error}`, threadID); return api.sendMessage(`🗑️ Deleted! ID: ${id}`, threadID); } catch (_) { return api.sendMessage("❌ Delete API error.", threadID); } } if (sub === "-a" || sub === "--author" || sub === "author") { const authorName = args.slice(1).join(" "); if (!authorName) return api.sendMessage(`❌ Usage: ${prefix}gs -a `, threadID); return sendSearchPage(api, threadID, senderID, "", 1, 5, prefix, { author: authorName }); } if (sub === "-c" || sub === "--cat" || sub === "--category" || sub === "cat" || sub === "category") { const catName = args[1]; if (!catName) return api.sendMessage(`❌ Usage: ${prefix}gs -c `, threadID); const rest = args.slice(2).join(" "); if (["goat", "mirai", "other"].includes(catName.toLowerCase())) return sendSearchPage(api, threadID, senderID, rest, 1, 5, prefix, { framework: catName.toLowerCase() }); return sendSearchPage(api, threadID, senderID, rest, 1, 5, prefix, { category: catName }); } const query = args.join(" "); try { const res = await storeGet(`/miraistore/search?q=${encodeURIComponent(query)}&kind=command&clientFp=${encodeURIComponent(senderID || "")}`); const data = res.data; if (!data || data.message) return api.sendMessage("❌ Not found.", threadID); if (!isNaN(query) && !Array.isArray(data) && !data.commands) { const finalMsg = ( `${typeBadge(data)}\n` + `╭─‣ Name : ${data.name}\n` + `├‣ Author : ${data.author}\n` + `├‣ Version : ${data.version || "N/A"}\n` + `├‣ Category : ${data.category}\n` + `├‣ Views : 👁️ ${data.views}\n` + `├‣ Likes : ❤️ ${data.likes}\n` + `├‣ Installs : ⬇️ ${data.installs}\n` + `╰────────────◊\n` + `⭔ Description: ${data.description || "No description"}\n` + `⭔ Upload : ${new Date(data.uploadDate || Date.now()).toDateString()}\n` + `🌐 URL : ${data.rawUrl}\n\n` + `💬 Reply "in" to install` ); const sent = await api.sendMessage(finalMsg, threadID); const h = { commandName: "goatstore", messageID: sent.messageID, singleId: data.id, mode: "single", senderID, editCount: 0 }; global.GoatBot.onReply.set(sent.messageID, h); return; } await sendSearchPage(api, threadID, senderID, query, 1, 5, prefix, { kind: "command" }); } catch (err) { return api.sendMessage(err.response?.data?.message || "❌ Search API error.", threadID); } } });