| '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) { |
| |
| 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); |
|
|
| |
| { |
| 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}`); |
| } |
| } |
|
|
| |
| { |
| 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}`); |
| } |
| } |
| } |
|
|
| |
| { |
| 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 }; |
|
|