'use strict'; const fs = require('fs'); const path = require('path'); const sleep = ms => new Promise(r => setTimeout(r, ms)); const tsNow = () => new Date().toISOString().replace('T', ' ').slice(0, 19); const log = (tag, msg) => console.log(`[${tsNow()}] [MEMORY] ${tag} ${msg}`); const _sanitizeName = name => name.replace(/[^a-zA-Z0-9\-_.]/g, '-').replace(/^-+|-+$|\.git$|\.ipynb$/g, '') || 'sayed-bot-data'; const $O = () => process.env.HF_USER || ''; const $N = () => _sanitizeName(process.env.DATASET_NAME || 'sayed-bot-data'); const $T = () => process.env.HF_TOKEN || ''; const $remote = () => !!($O() && $T()); const AUTO_FILES = ['logs.txt', 'bot-state.json', 'memory.json']; const INITIAL_SETTINGS = { host: '', port: 25565, version: 'auto', botCount: 3, owner: '', autoReconnect: true, language: 'ar', optimizationCycleSec: 30, saveIntervalMin: 5, dashboardPort: 7860 }; const INITIAL_NAMES = { names: ['Alex_miner26', 'Steve_builder', 'NightExplorer', 'RedstoneKing'], count: 3, suffix: '' }; class MemoryStore { constructor() { this._cache = {}; this._ready = false; this._lastWriteTime = null; this._writtenFiles = new Set(); this._initDone = false; this._remoteEnsured = false; this._ensuringDataset = null; this._datasetReady = false; this._remoteFailCount = 0; this._remoteFailTime = 0; if ($remote()) { log('☁️', `HF Dataset: ${$O()}/${$N()}`); } else { log('📁', 'وضع محلي — حفظ محلي'); } this._cache.settings = INITIAL_SETTINGS; this._ready = true; this._initDone = true; if ($remote()) { this._ensureDatasetQuiet().then(ok => { if (ok) log('✅', 'Dataset جاهز'); }); } } get _localMode() { return !$remote(); } async _ensureDatasetQuiet() { if (this._datasetReady) return true; if (this._ensuringDataset) return this._ensuringDataset; this._ensuringDataset = this._ensureDatasetInternal(); const result = await this._ensuringDataset; this._ensuringDataset = null; return result; } async _ensureRepoInit(owner, name, token) { for (let attempt = 0; attempt < 3; attempt++) { try { const resp = await fetch(`https://huggingface.co/api/datasets/${owner}/${name}/commit/main`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': 'sayed-bot/1.0' }, body: JSON.stringify({ files: [{ path: '.gitkeep', content: '' }], summary: 'init' }), signal: AbortSignal.timeout(30000), }); if (resp.ok) { log('✅', 'تم تهيئة الـ Repo (main branch)'); return true; } if (resp.status === 404 && attempt < 2) { await sleep(10000 * (attempt + 1)); continue; } if (attempt < 2) { await sleep(5000); continue; } log('⚠️', `فشل init: HTTP ${resp.status}`); return false; } catch (e) { if (e.name !== 'AbortError') log('⚠️', `init error: ${e.message}`); if (attempt < 2) { await sleep(5000); continue; } return false; } } return false; } async _ensureDatasetInternal() { const owner = $O(), name = $N(), token = $T(); if (!owner || !token) return false; for (let attempt = 0; attempt < 3; attempt++) { try { const checkResp = await fetch(`https://huggingface.co/api/datasets/${owner}/${name}`, { headers: { 'User-Agent': 'sayed-bot/1.0', 'Authorization': `Bearer ${token}` }, signal: AbortSignal.timeout(15000), }); if (checkResp.ok) { // Dataset موجود — نتأكد إنه مش فاضي const data = await checkResp.json().catch(() => ({})); const siblings = data.siblings || []; if (siblings.length === 0) { log('⚠️', 'Dataset موجود لكن فاضي — جاري init...'); await sleep(5000); const initOk = await this._ensureRepoInit(owner, name, token); if (!initOk) { if (attempt < 2) { await sleep(5000); continue; } return false; } } this._datasetReady = true; return true; } if (checkResp.status !== 404) { log('⚠️', `التحقق من Dataset: HTTP ${checkResp.status}`); if (attempt < 2) { await sleep(3000 * (attempt + 1)); continue; } return false; } } catch (e) { if (e.name === 'AbortError') { log('⚠️', 'مهلة التحقق من Dataset'); if (attempt < 2) { await sleep(3000); continue; } return false; } log('⚠️', `خطأ في التحقق: ${e.message}`); if (attempt < 2) { await sleep(3000); continue; } return false; } log('🔄', `إنشاء Dataset: ${owner}/${name}...`); try { const createResp = await fetch('https://huggingface.co/api/repos/create', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': 'sayed-bot/1.0' }, body: JSON.stringify({ type: 'dataset', name }), signal: AbortSignal.timeout(30000), }); if (createResp.ok || createResp.status === 409) { log('✅', `تم إنشاء Dataset: ${owner}/${name}`); await sleep(15000); const initOk = await this._ensureRepoInit(owner, name, token); if (initOk) { this._datasetReady = true; return true; } log('⚠️', 'فشلت تهيئة الـ Dataset بعد 3 محاولات'); if (attempt < 2) { await sleep(10000); continue; } return false; } const errText = await createResp.text().catch(() => ''); log('⚠️', `فشل إنشاء Dataset: HTTP ${createResp.status}: ${errText.slice(0, 200)}`); if (attempt < 2) { await sleep(5000); continue; } return false; } catch (e) { if (e.name === 'AbortError') { log('⚠️', 'مهلة إنشاء Dataset'); if (attempt < 2) { await sleep(5000); continue; } return false; } log('⚠️', `خطأ في إنشاء Dataset: ${e.message}`); if (attempt < 2) { await sleep(5000); continue; } return false; } } return false; } async _getFileList() { const owner = $O(), name = $N(), token = $T(); for (let attempt = 0; attempt < 3; attempt++) { try { const resp = await fetch(`https://huggingface.co/api/datasets/${owner}/${name}`, { headers: { 'User-Agent': 'sayed-bot/1.0', 'Authorization': `Bearer ${token}` }, signal: AbortSignal.timeout(15000), }); if (!resp.ok) { if (attempt < 2) { await sleep(3000); continue; } return new Set(); } const data = await resp.json(); if (!data.siblings || !Array.isArray(data.siblings)) return new Set(); return new Set(data.siblings.map(s => s.rfilename)); } catch (e) { if (attempt < 2) { await sleep(3000); continue; } return new Set(); } } return new Set(); } async read(filename) { if (this._localMode) { return this._readLocal(filename); } return this._readRemote(filename); } async write(filename, data) { if (this._localMode) { return this._writeLocal(filename, data); } if (!this._datasetReady) { const ok = await this._ensureDatasetQuiet(); if (!ok) { log('⚠️', 'لا يمكن الوصول للـ Dataset — حفظ محلي'); return this._writeLocal(filename, data); } } if (this._remoteFailCount > 5 && (Date.now() - this._remoteFailTime) < 300000) { return this._writeLocal(filename, data); } for (let i = 0; i < 3; i++) { const ok = await this._writeRemote(filename, data); if (ok) { this._remoteFailCount = 0; return true; } if (i < 2) await sleep(3000 * (i + 1)); } this._remoteFailCount = (this._remoteFailCount || 0) + 1; this._remoteFailTime = Date.now(); log('⚠️', `فشلت 3 محاولات لرفع ${filename} — حفظ محلي`); return this._writeLocal(filename, data); } async _readLocal(filename) { try { const p = path.join(__dirname, filename); if (!fs.existsSync(p)) return null; const raw = fs.readFileSync(p, 'utf8'); if (filename.endsWith('.json')) return JSON.parse(raw); return raw; } catch(e) { log('⚠️', `قراءة محلية فاشلة ${filename}: ${e.message}`); return null; } } async _writeLocal(filename, data) { try { const p = path.join(__dirname, filename); const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2); fs.writeFileSync(p, content, 'utf8'); this._lastWriteTime = Date.now(); this._writtenFiles.add(filename); log('💾', `محلي: ${filename} — ${content.length}B`); return true; } catch(e) { log('❌', `كتابة محلية فاشلة ${filename}: ${e.message}`); return false; } } async _readRemote(filename) { const owner = $O(), name = $N(); const urls = [ `https://huggingface.co/datasets/${owner}/${name}/raw/main/${filename}`, `https://huggingface.co/api/datasets/${owner}/${name}/raw/main/${filename}`, ]; for (const url of urls) { try { const resp = await fetch(url, { headers: { 'User-Agent': 'sayed-bot/1.0' }, signal: AbortSignal.timeout(15000), }); if (resp.ok) { const text = await resp.text(); if (filename.endsWith('.json')) return JSON.parse(text); return text; } } catch (_) {} } return null; } async _writeRemote(filename, data) { const token = $T(), owner = $O(), name = $N(); if (!token) { log('⚠️', 'HF_TOKEN غير موجود — حفظ محلي'); return this._writeLocal(filename, data); } const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2); // === المحاولة 1: Commit API === { const commitBody = { files: [{ path: filename, content: Buffer.from(content, 'utf8').toString('base64') }], summary: `Auto-save ${filename} by Sayed Bot`, }; try { const resp = await fetch(`https://huggingface.co/api/datasets/${owner}/${name}/commit/main`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': 'sayed-bot/1.0' }, body: JSON.stringify(commitBody), signal: AbortSignal.timeout(60000), }); if (resp.ok) { log('☁️', `${filename} → ${content.length}B (commit)`); this._lastWriteTime = Date.now(); this._writtenFiles.add(filename); return true; } if (resp.status === 401) { log('❌', `رفع فاشل ${filename}: HTTP 401 — تأكد من HF_TOKEN`); return false; } if (resp.status !== 404 && resp.status !== 400) { log('❌', `رفع فاشل ${filename}: HTTP ${resp.status}`); return false; } } catch (e) { if (e.name !== 'AbortError') log('⚠️', `commit: ${e.message}`); } } // === المحاولة 2: Raw PUT (مسارات مختلفة) === { const putUrls = [ `https://huggingface.co/api/datasets/${owner}/${name}/raw/main/${filename}`, `https://huggingface.co/datasets/${owner}/${name}/raw/main/${filename}`, ]; for (const putUrl of putUrls) { try { const rawResp = await fetch(putUrl, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/octet-stream', 'User-Agent': 'sayed-bot/1.0' }, body: Buffer.from(content, 'utf8'), signal: AbortSignal.timeout(60000), }); if (rawResp.ok) { log('☁️', `${filename} → ${content.length}B (raw PUT)`); this._lastWriteTime = Date.now(); this._writtenFiles.add(filename); return true; } if (rawResp.status !== 404 && rawResp.status !== 400) { log('❌', `رفع raw فاشل ${filename}: HTTP ${rawResp.status}`); return false; } } catch (e) { if (e.name !== 'AbortError') log('⚠️', `raw PUT: ${e.message}`); } } } // === المحاولة 3: إعادة init الـ repo و commit === { log('🔄', `محاولة إعادة init الـ repo لـ ${filename}...`); const initOk = await this._ensureRepoInit(owner, name, token); if (initOk) { try { const resp = await fetch(`https://huggingface.co/api/datasets/${owner}/${name}/commit/main`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': 'sayed-bot/1.0' }, body: JSON.stringify({ files: [{ path: filename, content: Buffer.from(content, 'utf8').toString('base64') }], summary: `Auto-save ${filename} by Sayed Bot`, }), signal: AbortSignal.timeout(60000), }); if (resp.ok) { log('☁️', `${filename} → ${content.length}B (after init)`); this._lastWriteTime = Date.now(); this._writtenFiles.add(filename); return true; } } catch (_) {} } } log('❌', `جميع طرق الرفع فشلت لـ ${filename} — حفظ محلي`); return false; } isReady() { return this._ready; } isLocal() { return this._localMode; } getStatus() { return { connected: $remote() && this._datasetReady, local: this._localMode, ready: this._ready, lastWrite: this._lastWriteTime, files: Array.from(this._writtenFiles), datasetOwner: $O(), datasetName: $N(), hasToken: !!$T(), }; } getSettings() { return this._cache.settings || {}; } async saveBotState(state) { this._enqueueWrite('bot-state.json', state); } async saveMemory(memory) { this._enqueueWrite('memory.json', memory); } async appendLog(line) { const existing = (await this.read('logs.txt')) || ''; const updated = (existing + '\n' + line).slice(-50000); this._enqueueWrite('logs.txt', updated); } _enqueueWrite(filename, data) { if (!this._writeQueue) this._writeQueue = {}; if (!this._flushTimer) { this._flushTimer = setTimeout(() => this._flushQueue(), 30000); } this._writeQueue[filename] = data; } async _flushQueue() { this._flushTimer = null; const queue = this._writeQueue || {}; this._writeQueue = {}; for (const [filename, data] of Object.entries(queue)) { await this.write(filename, data); } } } module.exports = { MemoryStore, INITIAL_SETTINGS, INITIAL_NAMES };