ayman-fca 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,15 +1,297 @@
1
1
  // ============================================================
2
- // AYMAN-FCA v2.0
3
- // Facebook Chat API for Node.js
2
+ // AYMAN-FCA v2.0 — ULTRA MASTER ENGINE
4
3
  // © 2025 Ayman. All Rights Reserved.
5
- // جميع الحقوق محفوظة لأيمن
6
4
  //
7
- // مكتبة مخصصة لبوت KIRA
8
- // تدوم بـ AppState واحد شهراً أو أكثر
5
+ // يجمع كل الأنظمة:
6
+ // SessionManager — جلسة + backup + validate
7
+ // ② ReconnectEngine — Circuit Breaker + backoff
8
+ // ③ KeepAliveEngine — نشاط حقيقي كل 4 دقائق
9
+ // ④ Watchdog — مراقبة MQTT + صمت
10
+ // ⑤ HealthMonitor — Score 0-100
11
+ // ⑥ MemoryManager — تنظيف ذاكرة
12
+ // ⑦ BehaviorEngine — محاكاة إنسان
13
+ // ⑧ SmartCooldown — تحكم ذكي بالسرعة
14
+ // ⑨ SilentRecovery — Soft Restart + Silent Mode
15
+ // ⑩ SmartMessageQueue— Queue ذكي
16
+ // ⑪ GeoGuard — حماية الموقع الجغرافي
9
17
  // ============================================================
18
+ "use strict";
10
19
 
11
- const login = require("./module/login");
20
+ const EventEmitter = require("events");
21
+ const path = require("path");
12
22
 
13
- module.exports = login;
14
- module.exports.login = login;
15
- module.exports.default = login;
23
+ const logger = require("../func/logger");
24
+ const SessionManager = require("./core/sessionManager");
25
+ const ReconnectEngine = require("./core/reconnectEngine");
26
+ const KeepAliveEngine = require("./core/keepAliveEngine");
27
+ const Watchdog = require("./core/watchdog");
28
+ const HealthMonitor = require("./core/healthMonitor");
29
+ const MemoryManager = require("./core/memoryManager");
30
+ const BehaviorEngine = require("./core/behaviorEngine");
31
+ const SmartCooldown = require("./core/smartCooldown");
32
+ const SilentRecovery = require("./core/silentRecovery");
33
+ const SmartMessageQueue = require("./core/smartMessageQueue");
34
+ const GeoGuard = require("./core/geoGuard");
35
+ const { EVENTS } = require("./system/constants");
36
+ const { isSessionError }= require("./system/errors");
37
+
38
+ class AymanFCAUltra extends EventEmitter {
39
+ constructor(options = {}) {
40
+ super();
41
+ this._appStatePath = options.appStatePath || path.join(process.cwd(), "appstate.json");
42
+ this._onMessage = options.onMessage || null;
43
+ this._api = null;
44
+ this._ctx = null;
45
+ this._restarting = false;
46
+ this._startedAt = Date.now();
47
+
48
+ // ── الأنظمة ──────────────────────────────────────────────
49
+ this.session = new SessionManager({ primaryPath: this._appStatePath, onSave: options.onSave || null });
50
+ this.reconnect = new ReconnectEngine();
51
+ this.keepAlive = new KeepAliveEngine();
52
+ this.watchdog = new Watchdog();
53
+ this.health = new HealthMonitor();
54
+ this.memory = new MemoryManager();
55
+ this.behavior = new BehaviorEngine({ sessionStart: this._startedAt });
56
+ this.cooldown = new SmartCooldown();
57
+ this.silent = new SilentRecovery();
58
+ this.queue = new SmartMessageQueue({ noise: true });
59
+ this.geo = new GeoGuard({ lockRegion: true });
60
+
61
+ this._wireSystems();
62
+ logger.banner();
63
+ }
64
+
65
+ // ── ربط الأنظمة ببعضها ──────────────────────────────────
66
+ _wireSystems() {
67
+ // Watchdog → restart
68
+ this.watchdog.on(EVENTS.WATCHDOG_RESTART, ({ reasons }) => {
69
+ this.health.penalize("mqtt_dead");
70
+ this._restart("watchdog: " + reasons);
71
+ });
72
+
73
+ // Health Critical → restart
74
+ this.health.on(EVENTS.HEALTH_CRITICAL, ({ score }) => {
75
+ logger.error(`Health منخفض (${score}) — restart`, "ULTRA");
76
+ this._restart("health_critical");
77
+ });
78
+
79
+ // Session Expired → restart
80
+ this.session.on(EVENTS.SESSION_EXPIRED, () => {
81
+ this.health.penalize("session_expired");
82
+ this._restart("session_expired");
83
+ });
84
+
85
+ // Memory High → risk up + pause queue
86
+ this.memory.on(EVENTS.MEMORY_HIGH, () => {
87
+ this.health.penalize("memory_high");
88
+ this.queue.setRiskLevel("high");
89
+ });
90
+
91
+ // Silent Mode → pause queue
92
+ this.silent.on("silent:enter", () => {
93
+ this.queue.pause("silent_mode");
94
+ this.cooldown.recordError();
95
+ });
96
+ this.silent.on("silent:exit", () => {
97
+ this.queue.resume();
98
+ });
99
+
100
+ // Reconnect events
101
+ this.reconnect.on(EVENTS.RECONNECT_DONE, () => {
102
+ this.health.reward("reconnect_done");
103
+ this.cooldown.reset();
104
+ this.queue.setRiskLevel("normal");
105
+ });
106
+ this.reconnect.on(EVENTS.RECONNECT_FAIL, () => {
107
+ this.health.penalize("reconnect_fail");
108
+ });
109
+
110
+ // Geo instability
111
+ this.geo.on("region:unstable", () => {
112
+ logger.warn("GeoGuard: عدم استقرار — Silent Mode", "ULTRA");
113
+ this.silent.enterSilentMode("geo_instability");
114
+ });
115
+
116
+ // Cooldown → Smart Queue speed
117
+ // health low → risk up
118
+ this.health.on(EVENTS.HEALTH_LOW, () => {
119
+ this.queue.setRiskLevel("high");
120
+ this.cooldown.recordError();
121
+ });
122
+ }
123
+
124
+ // ── استخراج ctx ─────────────────────────────────────────
125
+ _extractCtx(api) {
126
+ for (const k of Object.getOwnPropertyNames(api)) {
127
+ try {
128
+ const v = api[k];
129
+ if (v && typeof v === "object" && v.jar && v.userID && v.userID !== "0") return v;
130
+ } catch(_) {}
131
+ }
132
+ return null;
133
+ }
134
+
135
+ // ── تشغيل كل الأنظمة بعد Login ─────────────────────────
136
+ _startSystems(api, ctx) {
137
+ this.session.attach(api);
138
+ this.keepAlive.attach(api, ctx);
139
+ this.watchdog.attach(api, ctx);
140
+ this.memory.registerCleanup(() => {
141
+ if (ctx?.tasks instanceof Map && ctx.tasks.size > 100) ctx.tasks.clear();
142
+ });
143
+
144
+ // تسجيل region
145
+ if (ctx?.region) this.geo.recordRegion(ctx.region);
146
+
147
+ // تشغيل الأنظمة
148
+ this.session.start();
149
+ this.keepAlive.start();
150
+ this.watchdog.start();
151
+ this.health.start();
152
+ this.memory.start();
153
+ this.queue.start();
154
+ this.behavior.start(ctx?.mqttClient);
155
+
156
+ logger.info("ULTRA: كل الأنظمة تعمل ✅", "ULTRA");
157
+ this.emit("ready", { uid: ctx?.userID });
158
+ }
159
+
160
+ // ── إيقاف كل الأنظمة ────────────────────────────────────
161
+ _stopSystems() {
162
+ ["session","keepAlive","watchdog","health","memory","queue","behavior","silent"].forEach(s => {
163
+ try { this[s].stop(); } catch(_) {}
164
+ });
165
+ }
166
+
167
+ // ── إعادة تشغيل ذكية (Soft Restart) ─────────────────────
168
+ async _restart(reason) {
169
+ if (this._restarting) return;
170
+ this._restarting = true;
171
+ logger.warn(`ULTRA: إعادة تشغيل — ${reason}`, "ULTRA");
172
+
173
+ // Soft restart بدل Hard restart
174
+ const ok = await this.silent.softRestart(async () => {
175
+ this._stopSystems();
176
+ try {
177
+ if (this._ctx?.mqttClient) {
178
+ this._ctx.mqttClient.removeAllListeners();
179
+ this._ctx.mqttClient.end(true);
180
+ }
181
+ } catch(_) {}
182
+ await this.reconnect.trigger(async () => {
183
+ await this.attachToApi(this._api);
184
+ }, new Error(reason));
185
+ }, reason);
186
+
187
+ if (!ok) logger.error("ULTRA: فشلت إعادة التشغيل", "ULTRA");
188
+ this._restarting = false;
189
+ }
190
+
191
+ // ── بناء Listener Callback ────────────────────────────────
192
+ buildListenerCallback() {
193
+ return (error, message) => {
194
+ if (error) {
195
+ if (error?.type === "stop_listen") return;
196
+
197
+ // Silent Mode إذا لزم
198
+ if (this.silent.shouldGoSilent(error)) {
199
+ this.silent.handleError(error, () => this._restart("silent_recovery"));
200
+ return;
201
+ }
202
+
203
+ if (error?.type === "account_inactive" || isSessionError(error)) {
204
+ this.health.penalize("session_expired");
205
+ this._restart("account_inactive");
206
+ return;
207
+ }
208
+
209
+ this.health.penalize("error");
210
+ this.cooldown.recordError();
211
+ this._restart("listen_error");
212
+ return;
213
+ }
214
+
215
+ if (!message) return;
216
+
217
+ // تحديث Watchdog + Health
218
+ this.watchdog.heartbeat();
219
+ this.health.reward("message_ok");
220
+ this.cooldown.recordSuccess();
221
+
222
+ // تحديث Geo إذا تغير
223
+ if (message.region) this.geo.recordRegion(message.region);
224
+
225
+ if (["presence","typ","read_receipt"].includes(message.type)) return;
226
+
227
+ if (this._onMessage) {
228
+ try { this._onMessage(null, message); } catch(e) {}
229
+ }
230
+ this.emit("message", message);
231
+ };
232
+ }
233
+
234
+ // ── ربط بـ API بعد Login ──────────────────────────────────
235
+ async attachToApi(api) {
236
+ this._api = api;
237
+ this._ctx = this._extractCtx(api);
238
+
239
+ if (this._ctx) {
240
+ api.ctx = this._ctx;
241
+ api.ctxMain = this._ctx;
242
+ logger.info(`ULTRA مرتبط | UID: ${this._ctx.userID} | Region: ${this._ctx.region || "?"}`, "ULTRA");
243
+ }
244
+
245
+ // Connection Warm-Up قبل أي نشاط
246
+ await this.behavior.warmUp();
247
+
248
+ // حفظ الجلسة فوراً
249
+ try { this.session.save(api.getAppState(), true); } catch(_) {}
250
+
251
+ // تشغيل الأنظمة
252
+ this._startSystems(api, this._ctx || {});
253
+
254
+ return api;
255
+ }
256
+
257
+ // ── wrap sendMessage بـ Queue + Cooldown ─────────────────
258
+ wrapSendMessage(api) {
259
+ const original = api.sendMessage.bind(api);
260
+ api.sendMessage = (msg, threadID, callback, messageID) => {
261
+ // Anti-Repeat
262
+ this.behavior.antiRepeatDelay("send", typeof msg === "string" ? msg.slice(0, 30) : "obj");
263
+
264
+ // Queue
265
+ this.queue.enqueue(async () => {
266
+ await this.cooldown.waitBeforeSend();
267
+ const start = Date.now();
268
+ return new Promise((res, rej) => {
269
+ original(msg, threadID, (err, info) => {
270
+ this.cooldown.recordLatency(Date.now() - start);
271
+ if (err) { this.cooldown.recordError(); if (callback) callback(err); rej(err); }
272
+ else { this.cooldown.recordSuccess(); if (callback) callback(null, info); res(info); }
273
+ }, messageID);
274
+ });
275
+ }, { threadID });
276
+ };
277
+ logger.info("ULTRA: sendMessage wrapped ✅", "ULTRA");
278
+ }
279
+
280
+ // ── إيقاف آمن ────────────────────────────────────────────
281
+ async stop() {
282
+ this._stopSystems();
283
+ try { if (this._api?.stopListening) this._api.stopListening(); } catch(_) {}
284
+ logger.info("ULTRA: موقوف ✅", "ULTRA");
285
+ }
286
+
287
+ // ── Getters ───────────────────────────────────────────────
288
+ get api() { return this._api; }
289
+ get ctx() { return this._ctx; }
290
+ getHealth() { return this.health.getStats(); }
291
+ getCooldown() { return this.cooldown.getStats(); }
292
+ getQueue() { return this.queue.getStats(); }
293
+ getGeo() { return this.geo.getStats(); }
294
+ getSilent() { return this.silent.getStats(); }
295
+ }
296
+
297
+ module.exports = AymanFCAUltra;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ayman-fca",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Facebook Chat API for Node.js — مكتبة KIRA بوت | © 2025 Ayman",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ const utils = require("../utils");
3
+ module.exports = function(defaultFuncs, api, ctx){
4
+ return function pinMessage(messageID, threadID, callback){
5
+ let resolveFunc, rejectFunc;
6
+ const returnPromise = new Promise((res,rej)=>{ resolveFunc=res; rejectFunc=rej; });
7
+ if (!callback) callback = (err,data)=> err ? rejectFunc(err) : resolveFunc(data);
8
+ defaultFuncs
9
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, {
10
+ doc_id: "2380599842158525",
11
+ variables: JSON.stringify({ messageID, threadID, isPinnedMessage: true })
12
+ }, ctx.globalOptions, ctx)
13
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
14
+ .then(resData=>{ if(resData.error) throw resData; callback(null,{success:true}); })
15
+ .catch(err=>callback(err));
16
+ return returnPromise;
17
+ };
18
+ };
19
+ forwardMessage.js:
20
+ "use strict";
21
+ const utils = require("../utils");
22
+ module.exports = function(defaultFuncs, api, ctx){
23
+ return function forwardMessage(messageID, threadID, callback){
24
+ let resolveFunc, rejectFunc;
25
+ const returnPromise = new Promise((res,rej)=>{ resolveFunc=res; rejectFunc=rej; });
26
+ if (!callback) callback = (err,data)=> err ? rejectFunc(err) : resolveFunc(data);
27
+ defaultFuncs
28
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, {
29
+ doc_id: "3218892511542012",
30
+ variables: JSON.stringify({
31
+ message: { forwarded_message_id: messageID },
32
+ thread_id: threadID
33
+ })
34
+ }, ctx.globalOptions, ctx)
35
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
36
+ .then(resData=>{ if(resData.error) throw resData; callback(null,{success:true}); })
37
+ .catch(err=>callback(err));
38
+ return returnPromise;
39
+ };
40
+ };
41
+ getRegion.js:
42
+ "use strict";
43
+ module.exports = function(defaultFuncs, api, ctx){
44
+ return function getRegion(){ return ctx?.region || null; };
45
+ };
@@ -0,0 +1,176 @@
1
+ // ============================================================
2
+ // AYMAN-FCA — BehaviorEngine
3
+ // الفكرة 1: Dynamic Human Behavior Simulation
4
+ // الفكرة 14: Idle Simulation
5
+ // الفكرة 15: Time-of-Day Awareness
6
+ // الفكرة 19: Behavior Drift
7
+ // الفكرة 23: Random Heartbeat Pattern
8
+ // ============================================================
9
+ "use strict";
10
+
11
+ const logger = require("../../func/logger");
12
+
13
+ // ── أوقات النشاط البشري ──────────────────────────────────────
14
+ const ACTIVITY_PROFILE = {
15
+ night: { hours: [0,1,2,3,4,5], multiplier: 0.2 }, // خامل جداً
16
+ morning: { hours: [6,7,8,9], multiplier: 0.7 }, // متوسط
17
+ day: { hours: [10,11,12,13,14,15,16,17], multiplier: 1.0 }, // طبيعي
18
+ evening: { hours: [18,19,20,21,22,23], multiplier: 0.8 } // أقل قليلاً
19
+ };
20
+
21
+ // ── Behavior Drift: النشاط يتطور بمرور الوقت ─────────────────
22
+ // الأسبوع الأول بطيء جداً، يزيد تدريجياً
23
+ function getDriftMultiplier(sessionStartMs) {
24
+ const ageDays = (Date.now() - sessionStartMs) / (1000 * 60 * 60 * 24);
25
+ if (ageDays < 1) return 0.3; // يوم أول: بطيء جداً
26
+ if (ageDays < 3) return 0.5; // 3 أيام: بطيء
27
+ if (ageDays < 7) return 0.7; // أسبوع: متوسط
28
+ if (ageDays < 14) return 0.9; // أسبوعين: قريب طبيعي
29
+ return 1.0; // بعدها: طبيعي
30
+ }
31
+
32
+ // ── الحصول على multiplier الوقت الحالي ──────────────────────
33
+ function getTimeMultiplier() {
34
+ const h = new Date().getHours();
35
+ for (const [, profile] of Object.entries(ACTIVITY_PROFILE)) {
36
+ if (profile.hours.includes(h)) return profile.multiplier;
37
+ }
38
+ return 1.0;
39
+ }
40
+
41
+ class BehaviorEngine {
42
+ constructor(options = {}) {
43
+ this._sessionStart = options.sessionStart || Date.now();
44
+ this._baseDelay = options.baseDelay || 2000;
45
+ this._patternMap = new Map(); // كشف التكرار
46
+ this._lastActivity = Date.now();
47
+ this._idleTimer = null;
48
+ this._active = false;
49
+ }
50
+
51
+ // ① delay بشري عشوائي مع مراعاة الوقت والعمر
52
+ humanDelay(min = 500, max = 3000) {
53
+ const timeMul = getTimeMultiplier();
54
+ const driftMul = getDriftMultiplier(this._sessionStart);
55
+ const combined = timeMul * driftMul;
56
+
57
+ const adjustedMin = Math.round(min / combined);
58
+ const adjustedMax = Math.round(max / combined);
59
+ const base = Math.floor(Math.random() * (adjustedMax - adjustedMin + 1)) + adjustedMin;
60
+
61
+ // jitter ±15%
62
+ const jitter = base * 0.15 * (Math.random() * 2 - 1);
63
+ const delay = Math.max(200, Math.round(base + jitter));
64
+
65
+ return new Promise(r => setTimeout(r, delay));
66
+ }
67
+
68
+ // ② كشف الأنماط المتكررة (الفكرة 12: Anti-Pattern Detection)
69
+ trackPattern(key, value) {
70
+ const k = `${key}:${value}`;
71
+ const count = (this._patternMap.get(k) || 0) + 1;
72
+ this._patternMap.set(k, count);
73
+
74
+ // تنظيف القديم كل 100 entry
75
+ if (this._patternMap.size > 100) {
76
+ const first = this._patternMap.keys().next().value;
77
+ this._patternMap.delete(first);
78
+ }
79
+
80
+ return count;
81
+ }
82
+
83
+ // إذا نفس الرسالة 3+ مرات → delay إضافي
84
+ async antiRepeatDelay(key, value) {
85
+ const count = this.trackPattern(key, value);
86
+ if (count >= 3) {
87
+ const extra = Math.min(count * 1000, 10000);
88
+ logger.warn(`Anti-Pattern: تكرار (${count}x) — delay ${extra}ms`, "BEHAVIOR");
89
+ await new Promise(r => setTimeout(r, extra));
90
+ }
91
+ }
92
+
93
+ // ③ Connection Warm-Up (الفكرة 13)
94
+ async warmUp() {
95
+ logger.info("Connection Warm-Up بدأ...", "BEHAVIOR");
96
+ await this.humanDelay(2000, 5000); // انتظر
97
+ logger.info("Warm-Up: read phase...", "BEHAVIOR");
98
+ await this.humanDelay(1000, 3000); // قراءة
99
+ logger.info("Warm-Up: idle phase...", "BEHAVIOR");
100
+ await this.humanDelay(3000, 8000); // خمول
101
+ logger.info("Warm-Up مكتمل ✅", "BEHAVIOR");
102
+ }
103
+
104
+ // ④ Idle Simulation - خمول عشوائي
105
+ scheduleIdleBreak(mqttClient) {
106
+ if (!this._active) return;
107
+ // كل 15-45 دقيقة خمول عشوائي من 2-10 دقائق
108
+ const nextIdleIn = (15 + Math.random() * 30) * 60 * 1000;
109
+
110
+ this._idleTimer = setTimeout(async () => {
111
+ if (!this._active) return;
112
+ const idleMs = (2 + Math.random() * 8) * 60 * 1000;
113
+ logger.info(`Idle Simulation: خمول ${Math.round(idleMs/60000)} دقيقة`, "BEHAVIOR");
114
+
115
+ // foreground = false أثناء الخمول
116
+ try {
117
+ if (mqttClient?.connected) {
118
+ mqttClient.publish("/foreground_state", JSON.stringify({ foreground: false }), { qos: 0 });
119
+ }
120
+ } catch(_) {}
121
+
122
+ await new Promise(r => setTimeout(r, idleMs));
123
+
124
+ // عودة للنشاط
125
+ try {
126
+ if (mqttClient?.connected) {
127
+ mqttClient.publish("/foreground_state", JSON.stringify({ foreground: true }), { qos: 0 });
128
+ }
129
+ } catch(_) {}
130
+
131
+ logger.info("Idle Simulation: عادة للنشاط ✅", "BEHAVIOR");
132
+ this.scheduleIdleBreak(mqttClient); // جدول التالي
133
+ }, nextIdleIn);
134
+ }
135
+
136
+ // ⑤ Random Heartbeat - نبضة غير ثابتة
137
+ getHeartbeatDelay() {
138
+ // بين 20-40 ثانية مع variation حسب الوقت
139
+ const base = 25000;
140
+ const range = 15000;
141
+ const timeMul = getTimeMultiplier();
142
+ return Math.round((base + Math.random() * range) / timeMul);
143
+ }
144
+
145
+ // ⑥ Session Age Awareness (الفكرة 2)
146
+ getSessionAge() {
147
+ return Date.now() - this._sessionStart;
148
+ }
149
+
150
+ isSessionAging() {
151
+ return this.getSessionAge() > 20 * 60 * 60 * 1000; // أكثر من 20 ساعة
152
+ }
153
+
154
+ // تقليل النشاط عند شيخوخة الجلسة
155
+ getAgedActivityMultiplier() {
156
+ const agingMs = this.getSessionAge();
157
+ const agingH = agingMs / (60 * 60 * 1000);
158
+ if (agingH < 12) return 1.0;
159
+ if (agingH < 20) return 0.8;
160
+ if (agingH < 30) return 0.6;
161
+ return 0.4; // بعد 30 ساعة: نشاط منخفض جداً
162
+ }
163
+
164
+ start(mqttClient) {
165
+ this._active = true;
166
+ this.scheduleIdleBreak(mqttClient);
167
+ logger.info("BehaviorEngine: مفعّل ✅", "BEHAVIOR");
168
+ }
169
+
170
+ stop() {
171
+ this._active = false;
172
+ if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; }
173
+ }
174
+ }
175
+
176
+ module.exports = BehaviorEngine;
@@ -0,0 +1,87 @@
1
+ // ============================================================
2
+ // AYMAN-FCA — GeoStabilityGuard
3
+ // الفكرة 6: Multi-Region Awareness
4
+ // الفكرة 24: Geo Stability Guard
5
+ // ============================================================
6
+ "use strict";
7
+
8
+ const EventEmitter = require("events");
9
+ const logger = require("../../func/logger");
10
+
11
+ class GeoStabilityGuard extends EventEmitter {
12
+ constructor(options = {}) {
13
+ super();
14
+ this._lastRegion = null;
15
+ this._regionChanges= 0;
16
+ this._maxChanges = options.maxChanges || 3;
17
+ this._history = [];
18
+ this._lockRegion = options.lockRegion || false;
19
+ }
20
+
21
+ // ── تسجيل Region ─────────────────────────────────────────
22
+ recordRegion(region) {
23
+ if (!region) return;
24
+
25
+ this._history.push({ region, ts: Date.now() });
26
+ if (this._history.length > 20) this._history.shift();
27
+
28
+ if (this._lastRegion && this._lastRegion !== region) {
29
+ this._regionChanges++;
30
+ logger.warn(`GeoGuard: Region تغير ${this._lastRegion} → ${region} (#${this._regionChanges})`, "GEO");
31
+ this.emit("region:changed", { from: this._lastRegion, to: region });
32
+
33
+ if (this._regionChanges >= this._maxChanges) {
34
+ logger.error("GeoGuard: تغييرات كثيرة — خطر logout", "GEO");
35
+ this.emit("region:unstable", { changes: this._regionChanges });
36
+ }
37
+ }
38
+
39
+ this._lastRegion = region;
40
+ }
41
+
42
+ // ── هل الموقع مستقر؟ ──────────────────────────────────────
43
+ isStable() {
44
+ return this._regionChanges < this._maxChanges;
45
+ }
46
+
47
+ // ── الحصول على Region الموصى به ───────────────────────────
48
+ getPreferredRegion() {
49
+ if (!this._history.length) return null;
50
+ // الـ region الأكثر تكراراً
51
+ const counts = {};
52
+ for (const { region } of this._history) {
53
+ counts[region] = (counts[region] || 0) + 1;
54
+ }
55
+ return Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] || null;
56
+ }
57
+
58
+ // ── تطبيق على ctx ──────────────────────────────────────────
59
+ applyToCtx(ctx) {
60
+ if (!ctx) return;
61
+
62
+ // تسجيل region الحالي
63
+ if (ctx.region) this.recordRegion(ctx.region);
64
+
65
+ // إذا lock region، أعد تطبيق الـ region المفضل
66
+ if (this._lockRegion && this._lastRegion && !ctx.region) {
67
+ ctx.region = this._lastRegion;
68
+ logger.info(`GeoGuard: تم تثبيت region = ${ctx.region}`, "GEO");
69
+ }
70
+ }
71
+
72
+ reset() {
73
+ this._regionChanges = 0;
74
+ this._history = [];
75
+ }
76
+
77
+ getStats() {
78
+ return {
79
+ currentRegion: this._lastRegion,
80
+ regionChanges: this._regionChanges,
81
+ stable: this.isStable(),
82
+ preferred: this.getPreferredRegion()
83
+ };
84
+ }
85
+ }
86
+
87
+ module.exports = GeoStabilityGuard;