Opera8 commited on
Commit
3c4d09d
·
verified ·
1 Parent(s): 4a4e0ad

Delete .streamlit/index.js

Browse files
Files changed (1) hide show
  1. .streamlit/index.js +0 -643
.streamlit/index.js DELETED
@@ -1,643 +0,0 @@
1
- // server/index.js (نسخه بهینه شده: انتخاب تصادفی کلیدها + پایداری بالا)
2
- const express = require('express');
3
- const path = require('node:path');
4
- const fs = require('node:fs');
5
- const { WebSocketServer, WebSocket } = require('ws');
6
- const http = require('node:http');
7
- require('dotenv').config();
8
-
9
- const app = express();
10
- app.set('trust proxy', true);
11
-
12
- // ==================== مدیریت محدودیت زمانی رایگان روزانه (سه لایه) ====================
13
- // شناسایی هر کاربر با ترکیب سه لایه انجام می‌شود:
14
- // لایه ۱: آی‌پی کاربر
15
- // لایه ۲: اثر انگشت مرورگر/دستگاه (fingerprint) - از src/lib/fingerprint.ts
16
- // لایه ۳: یک شناسه ذخیره‌شده در حافظه مرورگر کاربر (localStorage)
17
- // اگر فقط یکی از این سه مورد با یکی از کاربران قبلی یکی باشد، همان پروفایل
18
- // (و همان اعتبار باقیمانده) به کاربر نسبت داده می‌شود. یعنی:
19
- // - پاک کردن حافظه مرورگر (لایه ۳) کمکی نمی‌کند، چون IP و fingerprint هنوز شناخته می‌شوند.
20
- // - باز کردن در تب ناشناس/مرورگر دیگر روی همان دستگاه و همان اینترنت هم چون IP یکی
21
- // می‌ماند و در بسیاری موارد fingerprint هم شبیه است، همان اعتبار قبلی نشان داده می‌شود.
22
- // همه چیز روی سرور محاسبه و ذخیره می‌شود، مرورگر کاربر هیچ نقشی در تعیین اعتبار واقعی ندارد.
23
-
24
- const DAILY_LIMIT_SECONDS = 300; // ۵ دقیقه
25
-
26
- // مسیر ذخیره‌سازی دائمی (باکت/دیسک پایدار هاگینگ‌فیس معمولاً روی /data مونت می‌شود)
27
- const PRIMARY_DATA_DIR = process.env.PERSIST_DATA_DIR || '/data';
28
- const FALLBACK_DATA_DIR = __dirname;
29
- const DB_FILE_NAME = 'sotifree.db';
30
-
31
- function resolveWritableDataDir() {
32
- try {
33
- fs.mkdirSync(PRIMARY_DATA_DIR, { recursive: true });
34
- const testFile = path.join(PRIMARY_DATA_DIR, '.write-test');
35
- fs.writeFileSync(testFile, 'ok');
36
- fs.unlinkSync(testFile);
37
- console.log(`✅ مسیر ذخیره‌سازی دائمی فعال است: ${PRIMARY_DATA_DIR}`);
38
- return PRIMARY_DATA_DIR;
39
- } catch (e) {
40
- console.warn(`⚠️ مسیر ${PRIMARY_DATA_DIR} در دسترس/قابل نوشتن نیست (${e.message})، استفاده از مسیر جایگزین: ${FALLBACK_DATA_DIR}`);
41
- return FALLBACK_DATA_DIR;
42
- }
43
- }
44
-
45
- const DATA_DIR = resolveWritableDataDir();
46
- const USAGE_FILE = path.join(DATA_DIR, DB_FILE_NAME);
47
-
48
- // ساختار داده در حافظه:
49
- // profiles: { [profileId]: { date: 'YYYY-MM-DD', usedSeconds: number } }
50
- // ipIndex / fpIndex / sidIndex: { [value]: profileId }
51
- let db = {
52
- profiles: {},
53
- ipIndex: {},
54
- fpIndex: {},
55
- sidIndex: {},
56
- };
57
-
58
- function loadDb() {
59
- try {
60
- if (fs.existsSync(USAGE_FILE)) {
61
- const raw = fs.readFileSync(USAGE_FILE, 'utf8');
62
- const parsed = JSON.parse(raw);
63
- if (parsed && typeof parsed === 'object') {
64
- db = {
65
- profiles: parsed.profiles || {},
66
- ipIndex: parsed.ipIndex || {},
67
- fpIndex: parsed.fpIndex || {},
68
- sidIndex: parsed.sidIndex || {},
69
- };
70
- }
71
- console.log(`✅ فایل اعتبار کاربران بارگذاری شد (${USAGE_FILE})`);
72
- } else {
73
- console.log(`ℹ️ فایل اعتبار کاربران وجود نداشت، یک فایل جدید ساخته می‌شود (${USAGE_FILE})`);
74
- }
75
- } catch (e) {
76
- console.error('⚠️ خطا در بارگذاری فایل اعتبار کاربران، شروع از صفر:', e.message);
77
- db = { profiles: {}, ipIndex: {}, fpIndex: {}, sidIndex: {} };
78
- }
79
- }
80
-
81
- let dirty = false;
82
- function markDirty() {
83
- dirty = true;
84
- }
85
-
86
- function flushDbToDisk() {
87
- if (!dirty) return;
88
- try {
89
- fs.writeFileSync(USAGE_FILE, JSON.stringify(db));
90
- dirty = false;
91
- } catch (e) {
92
- console.error('⚠️ خطا در ذخیره فایل اعتبار کاربران:', e.message);
93
- }
94
- }
95
-
96
- // طبق درخواست: چون هر بار فقط چند ثانیه از اعتبار کم می‌شود، نیازی نیست هر
97
- // ثانیه روی دیسک نوشته شود. هر ۳۰ ثانیه یک‌بار روی دیسک ذخیره می‌شود.
98
- const PERSIST_INTERVAL_MS = 30000;
99
- setInterval(flushDbToDisk, PERSIST_INTERVAL_MS);
100
-
101
- // ذخیره نهایی در زمان خاموش شدن سرور (تا حد امکان)
102
- process.on('SIGTERM', () => { flushDbToDisk(); process.exit(0); });
103
- process.on('SIGINT', () => { flushDbToDisk(); process.exit(0); });
104
-
105
- loadDb();
106
-
107
- function getTodayString() {
108
- // تاریخ روز جاری بر مبنای UTC، تا در ری‌استارت سرور دچار اختلاف منطقه زمانی نشویم
109
- return new Date().toISOString().slice(0, 10);
110
- }
111
-
112
- function getClientIp(req) {
113
- const forwarded = req.headers['x-forwarded-for'];
114
- if (forwarded) {
115
- return String(forwarded).split(',')[0].trim();
116
- }
117
- return (req.socket && req.socket.remoteAddress) || 'unknown-ip';
118
- }
119
-
120
- function normalizeKeyPart(value, maxLen) {
121
- if (value && typeof value === 'string' && value.length > 0 && value.length <= maxLen) {
122
- return value;
123
- }
124
- return null;
125
- }
126
-
127
- function createNewProfileId() {
128
- return 'p_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
129
- }
130
-
131
- function getUsedSecondsToday(profileId) {
132
- const profile = db.profiles[profileId];
133
- if (!profile) return 0;
134
- const today = getTodayString();
135
- if (profile.date !== today) return 0;
136
- return profile.usedSeconds || 0;
137
- }
138
-
139
- // پیدا کردن یا ساختن پروفایل کاربر بر اساس سه لایه (IP، fingerprint، شناسه حافظه مرورگر)
140
- // اگر هرکدام از این سه به یک پروفایل قبلی اشاره کنند، همان پروفایل (پروفایلی که
141
- // بیشترین اعتبار مصرف‌شده امروز را دارد، برای جلوگیری از دور زدن محدودیت) انتخاب می‌شود
142
- // و هر سه ایندکس به آن پروفایل واحد متصل می‌شوند.
143
- function resolveProfileId(ip, fp, sid) {
144
- const safeIp = normalizeKeyPart(ip, 100) || 'unknown-ip';
145
- const safeFp = normalizeKeyPart(fp, 200);
146
- const safeSid = normalizeKeyPart(sid, 200);
147
-
148
- const candidateIds = new Set();
149
- if (db.ipIndex[safeIp]) candidateIds.add(db.ipIndex[safeIp]);
150
- if (safeFp && db.fpIndex[safeFp]) candidateIds.add(db.fpIndex[safeFp]);
151
- if (safeSid && db.sidIndex[safeSid]) candidateIds.add(db.sidIndex[safeSid]);
152
-
153
- let profileId;
154
- if (candidateIds.size === 0) {
155
- profileId = createNewProfileId();
156
- db.profiles[profileId] = { date: getTodayString(), usedSeconds: 0, lastSeen: Date.now() };
157
- } else {
158
- // از بین همه پروفایل‌های مرتبط، پروفایلی که بیشترین مصرف امروز را دارد به عنوان
159
- // مرجع انتخاب می‌شود (رویکرد محافظه‌کارانه برای جلوگیری از دور زدن محدودیت)
160
- let maxUsed = -1;
161
- for (const id of candidateIds) {
162
- const used = getUsedSecondsToday(id);
163
- if (used > maxUsed) {
164
- maxUsed = used;
165
- profileId = id;
166
- }
167
- }
168
- if (!db.profiles[profileId] || db.profiles[profileId].date !== getTodayString()) {
169
- db.profiles[profileId] = { date: getTodayString(), usedSeconds: maxUsed > 0 ? maxUsed : 0, lastSeen: Date.now() };
170
- } else {
171
- db.profiles[profileId].lastSeen = Date.now();
172
- }
173
- }
174
-
175
- // اتصال هر سه لایه به همین پروفایل واحد
176
- if (db.ipIndex[safeIp] !== profileId) { db.ipIndex[safeIp] = profileId; markDirty(); }
177
- if (safeFp && db.fpIndex[safeFp] !== profileId) { db.fpIndex[safeFp] = profileId; markDirty(); }
178
- if (safeSid && db.sidIndex[safeSid] !== profileId) { db.sidIndex[safeSid] = profileId; markDirty(); }
179
-
180
- return profileId;
181
- }
182
-
183
- function getRemainingSeconds(ip, fp, sid) {
184
- const profileId = resolveProfileId(ip, fp, sid);
185
- const used = getUsedSecondsToday(profileId);
186
- return Math.max(0, DAILY_LIMIT_SECONDS - used);
187
- }
188
-
189
- function addUsageSeconds(ip, fp, sid, seconds) {
190
- const profileId = resolveProfileId(ip, fp, sid);
191
- const today = getTodayString();
192
- if (!db.profiles[profileId] || db.profiles[profileId].date !== today) {
193
- db.profiles[profileId] = { date: today, usedSeconds: 0, lastSeen: Date.now() };
194
- }
195
- db.profiles[profileId].usedSeconds += seconds;
196
- db.profiles[profileId].lastSeen = Date.now();
197
- markDirty();
198
- return Math.max(0, DAILY_LIMIT_SECONDS - db.profiles[profileId].usedSeconds);
199
- }
200
-
201
- // ---------- پاکسازی خودکار رکوردهای قدیمی‌تر از ۲۴ ساعت ----------
202
- // چون محدودیت، «روزانه» است، هیچ نیازی نیست اطلاعات کاربری که بیش از ۲۴
203
- // ساعت است سراغش نیامده در فایل sotifree.db نگه داشته شود. این تابع به‌طور
204
- // دوره‌ای هر پروفایلی که در ۲۴ ساعت اخیر هیچ فعالیتی نداشته را، به همراه
205
- // تمام ایندکس‌های (IP/fingerprint/sid) متصل به آن، کامل از فایل حذف می‌کند.
206
- const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
207
-
208
- function cleanupOldData() {
209
- const cutoff = Date.now() - TWENTY_FOUR_HOURS_MS;
210
- const staleProfileIds = new Set();
211
-
212
- for (const profileId of Object.keys(db.profiles)) {
213
- const profile = db.profiles[profileId];
214
- const lastSeen = typeof profile.lastSeen === 'number' ? profile.lastSeen : 0;
215
- if (lastSeen < cutoff) {
216
- staleProfileIds.add(profileId);
217
- }
218
- }
219
-
220
- if (staleProfileIds.size === 0) return;
221
-
222
- for (const profileId of staleProfileIds) {
223
- delete db.profiles[profileId];
224
- }
225
- for (const key of Object.keys(db.ipIndex)) {
226
- if (staleProfileIds.has(db.ipIndex[key])) delete db.ipIndex[key];
227
- }
228
- for (const key of Object.keys(db.fpIndex)) {
229
- if (staleProfileIds.has(db.fpIndex[key])) delete db.fpIndex[key];
230
- }
231
- for (const key of Object.keys(db.sidIndex)) {
232
- if (staleProfileIds.has(db.sidIndex[key])) delete db.sidIndex[key];
233
- }
234
-
235
- markDirty();
236
- flushDbToDisk();
237
- console.log(`🧹 پاکسازی خودکار: ${staleProfileIds.size} رکورد کاربر که بیش از ۲۴ ساعت بی‌استفاده بودند، از فایل ${USAGE_FILE} حذف شدند.`);
238
- }
239
-
240
- // یک‌بار در لحظه بالا آمدن سرور، و از آن به بعد هر یک ساعت یک‌بار اجرا می‌شود
241
- cleanupOldData();
242
- const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // هر ۱ ساعت
243
- setInterval(cleanupOldData, CLEANUP_INTERVAL_MS);
244
- // ================================================================================
245
-
246
- // افزایش محدودیت‌ها برای جلوگیری از خطای هدر در درخواست‌های سنگین
247
- const server = http.createServer({
248
- maxHeaderSize: 16384 // 16KB
249
- }, app);
250
-
251
- const wss = new WebSocketServer({
252
- server,
253
- clientTracking: true, // برای مدیریت هارت‌بیت و بستن اتصالات مرده
254
- perMessageDeflate: false // غیرفعال کردن فشرده‌سازی برای کاهش بار CPU و حافظه
255
- });
256
-
257
- // --- بخش مدیریت تنظیمات شخصیت‌ها ---
258
- const instructionSecretNames = {
259
- default: 'PERSONALITY_DEFAULT',
260
- teacher: 'PERSONALITY_TEACHER',
261
- poetic: 'PERSONALITY_POETIC',
262
- funny: 'PERSONALITY_FUNNY',
263
- };
264
-
265
- const personalityInstructions = {};
266
- console.log('🔄 در حال خواندن دستورالعمل‌های شخصیت از Secrets...');
267
- Object.keys(instructionSecretNames).forEach(key => {
268
- const secretName = instructionSecretNames[key];
269
- const instruction = process.env[secretName];
270
- if (instruction) {
271
- personalityInstructions[key] = instruction;
272
- } else {
273
- personalityInstructions[key] = `دستورالعمل '${key}' یافت نشد.`;
274
- }
275
- });
276
-
277
- // --- بخش مدیریت کلیدهای API (Random Selection) ---
278
- const apiKeysEnv = process.env.ALL_GEMINI_API_KEYS;
279
- // تبدیل رشته کلیدها به آرایه و حذف فاصله‌های اضافی
280
- const apiKeys = apiKeysEnv ? apiKeysEnv.split(',').map(key => key.trim()).filter(key => key) : [];
281
-
282
- if (apiKeys.length === 0) {
283
- console.error('🔴 خطای حیاتی: هیچ کلید API یافت نشد! لطفا متغیر ALL_GEMINI_API_KEYS را تنظیم کنید.');
284
- process.exit(1);
285
- }
286
- console.log(`🚀 سرور با ${apiKeys.length} کلید API آماده‌سازی شد. (حالت انتخاب تصادفی)`);
287
-
288
- // --- مکانیزم Heartbeat برای جلوگیری از قطعی و هنگ کردن سرور ---
289
- // این تابع هر ۳۰ ثانیه اتصالات مرده را پاک می‌کند تا حافظه آزاد شود
290
- function heartbeat() {
291
- this.isAlive = true;
292
- }
293
-
294
- const interval = setInterval(function ping() {
295
- wss.clients.forEach(function each(ws) {
296
- if (ws.isAlive === false) {
297
- // اگر کلاینت پاسخ نداد، اتصال را قطع کن تا حافظه آزاد شود
298
- return ws.terminate();
299
- }
300
-
301
- ws.isAlive = false;
302
- ws.ping();
303
- });
304
- }, 30000);
305
-
306
- wss.on('close', function close() {
307
- clearInterval(interval);
308
- });
309
-
310
- // --- توابع اتصال به گوگل ---
311
-
312
- /**
313
- * اتصال سوکت کلاینت و سوکت جمینای را به هم متصل می‌کند
314
- */
315
- function attachGeminiEventHandlers(clientWs, geminiWs, apiKeyUsed) {
316
- // انتقال پیام از گوگل به کلاینت
317
- geminiWs.on('message', (data) => {
318
- if (clientWs.readyState === WebSocket.OPEN) {
319
- clientWs.send(data, { binary: true });
320
- }
321
- });
322
-
323
- geminiWs.on('error', (error) => {
324
- console.error(`🔴 خطای جمینای (کلید ...${apiKeyUsed.slice(-4)}):`, error.message);
325
- // در صورت خطا، سوکت کلاینت را نمی‌بندیم تا شاید بتواند دوباره تلاش کند،
326
- // اما معمولا کلاینت خودش قطع می‌شود.
327
- if (clientWs.readyState === WebSocket.OPEN) clientWs.close();
328
- });
329
-
330
- geminiWs.on('close', (code) => {
331
- if (clientWs.readyState === WebSocket.OPEN) clientWs.close();
332
- });
333
-
334
- geminiWs.on('ping', () => {
335
- try { geminiWs.pong(); } catch (e) {}
336
- });
337
- }
338
-
339
- /**
340
- * تلاش برای اتصال به جمینای با انتخاب تصادفی و امتحان مجدد
341
- * @param {WebSocket} clientWs سوکت کاربر
342
- * @param {Object} setupData داده‌های اولیه (کانفیگ)
343
- * @param {Number} startIndex ایندکس شروع (که به صورت تصادفی انتخاب شده)
344
- * @param {Number} attemptCount شمارنده تلاش‌ها
345
- */
346
- async function tryConnectToGemini(clientWs, setupData, startIndex, attemptCount = 0) {
347
- // اگر کلاینت در حین تلاش قطع شد، ادامه نده (جلوگیری از لیک حافظه)
348
- if (clientWs.readyState !== WebSocket.OPEN) return null;
349
-
350
- // اگر تمام کلیدها تست شدند و هیچکدام کار نکرد
351
- if (attemptCount >= apiKeys.length) {
352
- console.error(`⛔ تمام ${apiKeys.length} کلید API با شکست مواجه شدند.`);
353
- if (clientWs.readyState === WebSocket.OPEN) {
354
- clientWs.send(JSON.stringify({ error: "سرویس موقتاً در دسترس نیست (ظرفیت تکمیل)." }));
355
- clientWs.close();
356
- }
357
- return null;
358
- }
359
-
360
- // منطق انتخاب کلید:
361
- // بار اول: ایندکس کاملا تصادفی (startIndex)
362
- // دفعات بعد (در صورت خطا): کلید بعدی در لیست به صورت چرخشی
363
- const keyIndexToTry = (startIndex + attemptCount) % apiKeys.length;
364
- const apiKey = apiKeys[keyIndexToTry];
365
-
366
- const url = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=${apiKey}`;
367
-
368
- return new Promise((resolve) => {
369
- const geminiWs = new WebSocket(url);
370
-
371
- // تایم‌اوت سخت: اگر گوگل تا ۸ ثانیه هیچ پاسخی نداد (هنگ کرد)، برو کلید بعدی
372
- const timeout = setTimeout(() => {
373
- console.warn(`⏳ تایم‌اوت اتصال (کلید ...${apiKey.slice(-4)}). رفتن به کلید بعدی...`);
374
- geminiWs.terminate();
375
- resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
376
- }, 8000);
377
-
378
- geminiWs.on('open', () => {
379
- clearTimeout(timeout);
380
-
381
- if (clientWs.readyState !== WebSocket.OPEN) {
382
- geminiWs.close();
383
- return resolve(null);
384
- }
385
-
386
- console.log(`🔗 اتصال موفق با کلید شماره ${keyIndexToTry} (تلاش ${attemptCount + 1})`);
387
-
388
- try {
389
- geminiWs.send(JSON.stringify(setupData));
390
- attachGeminiEventHandlers(clientWs, geminiWs, apiKey);
391
- resolve(geminiWs);
392
- } catch (e) {
393
- console.error("خطا در ارسال تنظیمات اولیه:", e);
394
- geminiWs.close();
395
- resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
396
- }
397
- });
398
-
399
- geminiWs.on('error', (err) => {
400
- clearTimeout(timeout);
401
- console.warn(`⚠️ خطای اتصال (کلید ...${apiKey.slice(-4)}): ${err.message || 'Unknown'}. تلاش با کلید بعدی...`);
402
- // فراخوانی بازگشتی برای تست کلید بعدی
403
- resolve(tryConnectToGemini(clientWs, setupData, startIndex, attemptCount + 1));
404
- });
405
- });
406
- }
407
-
408
- // --- سرو کردن فایل‌های استاتیک ---
409
- app.use(express.static(path.join(__dirname, '../build')));
410
-
411
- // API Endpoint دستورالعمل‌ها
412
- app.get('/api/instructions', (req, res) => {
413
- res.setHeader('Cache-Control', 'no-store');
414
- res.json(personalityInstructions);
415
- });
416
-
417
- // API Endpoint استعلام زمان باقیمانده رایگان روزانه (سه لایه: IP + fingerprint + شناسه حافظه مرورگر)
418
- app.get('/api/usage', (req, res) => {
419
- res.setHeader('Cache-Control', 'no-store');
420
- const ip = getClientIp(req);
421
- const fp = req.query.fp;
422
- const sid = req.query.sid;
423
- const remainingSeconds = getRemainingSeconds(ip, fp, sid);
424
- res.json({ remainingSeconds, limitSeconds: DAILY_LIMIT_SECONDS });
425
- });
426
-
427
- // --- مدیریت WebSocket کلاینت ---
428
- wss.on('connection', (ws, req) => {
429
- ws.isAlive = true;
430
- ws.on('pong', heartbeat);
431
-
432
- // --- شناسایی کاربر بر اساس سه لایه: IP + اثر انگشت مرورگر + شناسه حافظه مرورگر ---
433
- const ip = getClientIp(req);
434
- let fp = null;
435
- let sid = null;
436
- try {
437
- const parsedUrl = new URL(req.url, 'http://localhost');
438
- fp = parsedUrl.searchParams.get('fp');
439
- sid = parsedUrl.searchParams.get('sid');
440
- } catch (e) {
441
- fp = null;
442
- sid = null;
443
- }
444
-
445
- // اگر همین الان هم زمان کاربر برای امروز تمام شده، اصلا اجازه اتصال نده
446
- if (getRemainingSeconds(ip, fp, sid) <= 0) {
447
- try {
448
- ws.send(JSON.stringify({ type: 'limitreached' }));
449
- } catch (e) {}
450
- ws.close(4001, 'DAILY_LIMIT_REACHED');
451
- return;
452
- }
453
-
454
- // *تغییر اصلی:* انتخاب یک نقطه شروع کاملاً تصادفی برای هر کاربر
455
- // این کار باعث می‌شود بار روی سرور پخش شود و نیاز به حافظه برای نگهداری نوبت نباشد.
456
- const randomStartIndex = Math.floor(Math.random() * apiKeys.length);
457
-
458
- let geminiWs = null;
459
- let isConnecting = false;
460
- let usageIntervalRef = null;
461
- let micActive = false; // آخرین وضعیتی که کلاینت اعلام کرده (مکالمه فعال است یا نه)
462
- let lastActiveHeartbeatAt = 0; // زمان آخرین باری که کلاینت واقعا (با اجرای جاوااسکریپت) اعلام «هنوز فعالم» کرد
463
- let lastRealtimeDataAt = 0; // زمان آخرین باری که داده صوتی/تصویری واقعی از کلاینت دریافت شد
464
- let heartbeatWatchdogRef = null;
465
-
466
- // حداکثر فاصله مجاز بین دو هارتبیت متوالی کلاینت. کلاینت هر ۵ ثانیه یک بار
467
- // این سیگنال را می‌فرستد؛ اگر تا ۱۵ ثانیه (سه برابر فاصله عادی، برای
468
- // اطمینان از نوسانات طبیعی شبکه) هیچ هارتبیت جدیدی نرسد، یعنی به احتمال
469
- // زیاد صفحه/برنامه به پس‌زمینه رفته و جاوااسکریپتش متوقف شده. در این حالت
470
- // شمارش زمان به‌طور خودکار متوقف می‌شود تا اعتبار کاربر در پس‌زمینه هدر نرود.
471
- const HEARTBEAT_TIMEOUT_MS = 15000;
472
-
473
- function stopUsageTracking() {
474
- if (usageIntervalRef) {
475
- clearInterval(usageIntervalRef);
476
- usageIntervalRef = null;
477
- }
478
- }
479
-
480
- function forceCloseForLimit() {
481
- stopUsageTracking();
482
- try {
483
- if (ws.readyState === WebSocket.OPEN) {
484
- ws.send(JSON.stringify({ type: 'limitreached' }));
485
- ws.close(4001, 'DAILY_LIMIT_REACHED');
486
- }
487
- } catch (e) {}
488
- if (geminiWs) {
489
- try { geminiWs.terminate(); } catch (e) {}
490
- geminiWs = null;
491
- }
492
- }
493
-
494
- // این شمارنده هر ثانیه یک ثانیه از سهمیه واقعی کاربر روی سرور (در حافظه) کم می‌کند
495
- // و مقدار باقیمانده را برای همگام‌سازی نمایش تایمر به کلاینت ارسال می‌کند.
496
- // ذخیره‌سازی روی دیسک/باکت هر ۳۰ ثانیه یک‌بار انجام می‌شود (نه هر ثانیه)، اما شمارش
497
- // و اعمال محدودیت همیشه لحظه‌به‌لحظه و سمت سرور است، پس قابل دور زدن نیست.
498
- //
499
- // *مهم - تضمین اتمی*: این شمارنده فقط زمانی واقعا اعتبار کم می‌کند که در
500
- // ۱۰ ثانیه اخیر داده صوتی/تصویری واقعی از کلاینت دریافت شده باشد (یا در
501
- // ۱۰ ثانیه اول بعد از شروع، به‌عنوان فرصت راه‌اندازی). این یعنی شمارش به
502
- // «وجود واقعی و قابل‌اثبات جریان مکالمه» گره خورده، نه صرفا یک پرچم که
503
- // ممکن است به هر دلیلی (باگ، پیام گم‌شده، تاخیر شبکه) اشتباه بماند. اگر
504
- // داده صوتی واقعی قطع شود، شمارش خودش و بدون نیاز به هیچ پیام دیگری متوقف می‌شود.
505
- const REALTIME_DATA_TIMEOUT_MS = 10000;
506
-
507
- function startUsageTracking() {
508
- if (usageIntervalRef) return;
509
- const trackingStartedAt = Date.now();
510
- usageIntervalRef = setInterval(() => {
511
- const hasRecentRealtimeData = lastRealtimeDataAt > 0 && (Date.now() - lastRealtimeDataAt) < REALTIME_DATA_TIMEOUT_MS;
512
- const withinStartupGrace = (Date.now() - trackingStartedAt) < REALTIME_DATA_TIMEOUT_MS;
513
- if (!hasRecentRealtimeData && !withinStartupGrace) {
514
- // مدتی است هیچ داده صوتی/تصویری واقعی‌ای دریافت نشده؛ یعنی عملا
515
- // مکالمه‌ای در جریان نیست، پس شمارش را قطعا متوقف کن
516
- micActive = false;
517
- stopUsageTracking();
518
- return;
519
- }
520
- const remaining = addUsageSeconds(ip, fp, sid, 1);
521
- if (ws.readyState === WebSocket.OPEN) {
522
- try {
523
- ws.send(JSON.stringify({ type: 'usage', remainingSeconds: remaining, limitSeconds: DAILY_LIMIT_SECONDS }));
524
- } catch (e) {}
525
- }
526
- if (remaining <= 0) {
527
- forceCloseForLimit();
528
- }
529
- }, 1000);
530
- }
531
-
532
- // این تابع وضعیت واقعی شمارش زمان را با «آخرین سیگنالی که کلاینت فرستاده» و
533
- // «وضعیت فعلی اتصال به Gemini» هماهنگ می‌کند. چون اتصال به Gemini کمی طول
534
- // می‌کشد، ممکن است سیگنال «مکالمه فعال است» کمی زودتر از برقرار شدن اتصال
535
- // برسد؛ این تابع تضمین می‌کند در آن حالت هم به‌محض آماده شدن اتصال، شمارش
536
- // به‌درستی شروع شود.
537
- function syncUsageTracking() {
538
- if (micActive && geminiWs && getRemainingSeconds(ip, fp, sid) > 0) {
539
- startUsageTracking();
540
- } else {
541
- stopUsageTracking();
542
- }
543
- }
544
-
545
- // *نگهبان هارتبیت*: صرفِ باز بودن اتصال WebSocket (که با پینگ/پونگ خودکار
546
- // مرورگر حتی در پس‌زمینه هم زنده نشان داده می‌شود) دلیل کافی برای «در حال
547
- // مکالمه بودن» ن��ست. این نگهبان هر ۵ ثانیه بررسی می‌کند که آیا هارتبیت
548
- // واقعی و تازه‌ای (که فقط با اجرای جاوااسکریپت صفحه ارسال می‌شود) از کلاینت
549
- // رسیده یا نه؛ اگر نرسیده باشد، شمارش را متوقف می‌کند.
550
- heartbeatWatchdogRef = setInterval(() => {
551
- if (micActive && lastActiveHeartbeatAt > 0 && (Date.now() - lastActiveHeartbeatAt) > HEARTBEAT_TIMEOUT_MS) {
552
- micActive = false;
553
- stopUsageTracking();
554
- }
555
- }, 5000);
556
-
557
- ws.on('message', async (message) => {
558
- try {
559
- // بررسی مجدد قبل از پردازش هر پیام، برای جلوگیری از سوءاستفاده در لحظه آخر
560
- if (getRemainingSeconds(ip, fp, sid) <= 0) {
561
- forceCloseForLimit();
562
- return;
563
- }
564
-
565
- if (!Buffer.isBuffer(message) || message[0] === 123) {
566
- const msgStr = message.toString();
567
- if (msgStr.startsWith('{')) {
568
- const data = JSON.parse(msgStr);
569
- if (data.setup) {
570
- if (isConnecting || geminiWs) return;
571
- isConnecting = true;
572
- // ارسال ایندکس تصادفی به تابع اتصال
573
- geminiWs = await tryConnectToGemini(ws, data, randomStartIndex);
574
- isConnecting = false;
575
- if (geminiWs) {
576
- // اگر کلاینت از قبل (حتی چند لحظه زودتر) اعلام کرده بود که
577
- // مکالمه فعال است، همین الان که اتصال واقعا برقرار شد شمارش شروع می‌شود.
578
- syncUsageTracking();
579
- if (ws.readyState === WebSocket.OPEN) {
580
- ws.send(JSON.stringify({ type: 'usage', remainingSeconds: getRemainingSeconds(ip, fp, sid), limitSeconds: DAILY_LIMIT_SECONDS }));
581
- }
582
- }
583
- return;
584
- }
585
-
586
- if (data.type === 'usage_control') {
587
- // کلاینت دقیقا اعلام می‌کند که مکالمه الان فعال است یا نه.
588
- // شمارش زمان فقط و فقط بر اساس همین سیگنال شروع یا متوقف می‌شود،
589
- // نه صرفا باز بودن اتصال. به همین دلیل با قطع مکالمه (حتی اگر
590
- // اتصال WebSocket برای اتصال سریع‌تر دفعه بعد باز بماند)، دیگر
591
- // چیزی از اعتبار کاربر کم نمی‌شود.
592
- micActive = data.active === true;
593
- if (micActive) {
594
- lastActiveHeartbeatAt = Date.now();
595
- }
596
- syncUsageTracking();
597
- if (micActive && ws.readyState === WebSocket.OPEN) {
598
- ws.send(JSON.stringify({ type: 'usage', remainingSeconds: getRemainingSeconds(ip, fp, sid), limitSeconds: DAILY_LIMIT_SECONDS }));
599
- }
600
- return;
601
- }
602
- }
603
- }
604
-
605
- if (geminiWs && geminiWs.readyState === WebSocket.OPEN) {
606
- // این دقیقا لحظه‌ای است که داده صوتی/تصویری واقعی از کلاینت به سمت
607
- // Gemini فوروارد می‌شود؛ یعنی مکالمه واقعا در جریان است.
608
- lastRealtimeDataAt = Date.now();
609
- geminiWs.send(message);
610
- }
611
- } catch (e) {
612
- console.error("خطا در پردازش پیام کلاینت:", e.message);
613
- }
614
- });
615
-
616
- ws.on('close', () => {
617
- stopUsageTracking();
618
- if (heartbeatWatchdogRef) { clearInterval(heartbeatWatchdogRef); heartbeatWatchdogRef = null; }
619
- flushDbToDisk(); // ذخیره فوری روی دیسک/باکت در پایان هر تماس، برای اطمینان بیشتر
620
- if (geminiWs) {
621
- // بستن اجباری اتصال گوگل برای آزادسازی منابع
622
- try {
623
- geminiWs.terminate();
624
- } catch(e) {}
625
- geminiWs = null;
626
- }
627
- });
628
-
629
- ws.on('error', (error) => {
630
- stopUsageTracking();
631
- if (heartbeatWatchdogRef) { clearInterval(heartbeatWatchdogRef); heartbeatWatchdogRef = null; }
632
- if (geminiWs) {
633
- try { geminiWs.terminate(); } catch(e) {}
634
- geminiWs = null;
635
- }
636
- });
637
- });
638
-
639
- // ارسال تمام درخواست‌های دیگر به React
640
- app.get('*', (req, res) => res.sendFile(path.join(__dirname, '../build', 'index.html')));
641
-
642
- const PORT = process.env.PORT || 3001;
643
- server.listen(PORT, () => console.log(`🚀 سرور (حالت تصادفی) روی پورت ${PORT} اجرا شد.`));