nodebb-plugin-niki-loyalty 1.3.16 → 1.5.1

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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node:*)"
5
+ ]
6
+ }
7
+ }
package/library.js CHANGED
@@ -1,3 +1,4 @@
1
+ const crypto = require('crypto');
1
2
  const db = require.main.require('./src/database');
2
3
  const user = require.main.require('./src/user');
3
4
  const posts = require.main.require('./src/posts');
@@ -5,23 +6,33 @@ const routeHelpers = require.main.require('./src/controllers/helpers');
5
6
  const nconf = require.main.require('nconf');
6
7
  const socketHelpers = require.main.require('./src/socket.io/index');
7
8
  const SocketPlugins = require.main.require('./src/socket.io/plugins');
9
+ const groups = require.main.require('./src/groups');
8
10
  const Plugin = {};
9
11
 
12
+ // Ödül kullanabilecek gruplar
13
+ const WALLET_GROUPS = ['Premium', 'Lite', 'VIP'];
14
+
10
15
  // =========================
11
16
  // ⚙️ AYARLAR & KURALLAR (GAME LOGIC)
12
17
  // =========================
13
18
  const SETTINGS = {
14
- dailyCap: 70, // Günlük Maksimum Limit (Ne kadar kazanırsa kazansın buradan fazla alamaz) - 2x artırıldı
19
+ dailyCap: 35, // Günlük Maksimum Limit
15
20
  };
16
21
 
17
- // Puan Tablosu ve Limitleri (Toplam Potansiyel ~90 Puan) - 2x artırıldı
22
+ // Puan Tablosu ve Limitleri (Toplam Potansiyel ~45 Puan)
18
23
  const ACTIONS = {
19
- login: { points: 10, limit: 1, name: 'Günlük Giriş 👋' }, // 10 Puan (2x)
20
- new_topic: { points: 10, limit: 1, name: 'Yeni Konu 📝' }, // 10 Puan (2x)
21
- reply: { points: 10, limit: 2, name: 'Yorum Yazma 💬' }, // 10 x 2 = 20 Puan (2x)
22
- read: { points: 2, limit: 10, name: 'Konu Okuma 👀' }, // 2 x 10 = 20 Puan (2x)
23
- like_given: { points: 5, limit: 2, name: 'Beğeni Atma ❤️' }, // 5 x 2 = 10 Puan (2x)
24
- like_taken: { points: 10, limit: 2, name: 'Beğeni Alma 🌟' } // 10 x 2 = 20 Puan (2x)
24
+ login: { points: 5, limit: 1, name: 'Günlük Giriş 👋' }, // 5 Puan
25
+ new_topic: { points: 5, limit: 1, name: 'Yeni Konu 📝' }, // 5 Puan
26
+ reply: { points: 5, limit: 2, name: 'Yorum Yazma 💬' }, // 5 x 2 = 10 Puan
27
+ read: { points: 1, limit: 10, name: 'Konu Okuma 👀' }, // 1 x 10 = 10 Puan
28
+ like_given: { points: 2.5, limit: 2, name: 'Beğeni Atma ❤️' }, // 2.5 x 2 = 5 Puan
29
+ like_taken: { points: 5, limit: 2, name: 'Beğeni Alma 🌟' } // 5 x 2 = 10 Puan
30
+ };
31
+
32
+ // Grup Katılım Bonusları
33
+ const GROUP_BONUSES = {
34
+ 'Premium': 30,
35
+ 'VIP': 50,
25
36
  };
26
37
 
27
38
  // Ödüller
@@ -67,7 +78,6 @@ async function addKasaLog(staffUid, customerName, customerUid, rewardName, amoun
67
78
 
68
79
  // 🔥 MERKEZİ PUAN DAĞITIM FONKSİYONU 🔥
69
80
  // Bütün puan işlemleri buradan geçer, limitleri kontrol eder.
70
- // 🔥 MERKEZİ PUAN DAĞITIM FONKSİYONU 🔥
71
81
  async function awardDailyAction(uid, actionKey) {
72
82
  try {
73
83
  const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
@@ -108,6 +118,10 @@ async function awardDailyAction(uid, actionKey) {
108
118
  await db.incrObjectFieldBy(dailyScoreKey, 'score', pointsToGive);
109
119
  await db.incrObjectFieldBy(actionCountKey, actionKey, 1);
110
120
 
121
+ // Günlük anahtarlara TTL koy (48 saat) - eski anahtarların birikmesini önle
122
+ await db.expire(dailyScoreKey, 172800);
123
+ await db.expire(actionCountKey, 172800);
124
+
111
125
  // Logla
112
126
  await addUserLog(uid, 'earn', pointsToGive, rule.name);
113
127
 
@@ -218,7 +232,7 @@ Plugin.onUpvote = async function (data) {
218
232
  }
219
233
 
220
234
  // Like Alan Kazanır (Post sahibi - kendine beğeni atamaz):
221
- if (postOwnerUid && postOwnerUid !== voterUid) {
235
+ if (postOwnerUid && String(postOwnerUid) !== String(voterUid)) {
222
236
  const likeTakenKey = `niki:liked_taken:${postOwnerUid}:${today}`;
223
237
  const alreadyTaken = await db.isSetMember(likeTakenKey, pid.toString());
224
238
 
@@ -236,6 +250,46 @@ Plugin.onUpvote = async function (data) {
236
250
  };
237
251
 
238
252
 
253
+ // 5. GRUP KATILIM BONUSU (Premium / VIP)
254
+ Plugin.onGroupJoin = async function (data) {
255
+ try {
256
+ const groupName = data.groupName;
257
+ const uid = data.uid;
258
+ if (!groupName || !uid) return;
259
+
260
+ const bonus = GROUP_BONUSES[groupName];
261
+ if (!bonus) return; // Bu grup için bonus tanımlı değil
262
+
263
+ // Aynı gruba tekrar katılırsa çift puan vermesin
264
+ const flagKey = `niki:group_bonus:${uid}:${groupName}`;
265
+ const alreadyClaimed = await db.get(flagKey);
266
+ if (alreadyClaimed) {
267
+ console.log(`[Niki-Loyalty] Grup bonusu zaten alınmış. UID: ${uid}, Group: ${groupName}`);
268
+ return;
269
+ }
270
+
271
+ await user.incrementUserFieldBy(uid, 'niki_points', bonus);
272
+ await db.set(flagKey, '1');
273
+ await addUserLog(uid, 'earn', bonus, `${groupName} Grubu Katılım Bonusu 🎉`);
274
+
275
+ console.log(`[Niki-Loyalty] ✅ GRUP BONUSU! UID: ${uid}, Group: ${groupName}, Points: +${bonus}`);
276
+
277
+ // Socket bildirimi
278
+ try {
279
+ if (socketHelpers && socketHelpers.server && socketHelpers.server.sockets) {
280
+ socketHelpers.server.sockets.in('uid_' + uid).emit('event:niki_award', {
281
+ title: 'Grup Bonusu! 🎉',
282
+ message: `${groupName} grubuna katıldığın için <strong style="color:#ffd700">+${bonus} Puan</strong> kazandın!`,
283
+ });
284
+ }
285
+ } catch (socketErr) {
286
+ console.error('[Niki-Loyalty] Socket emit hatası:', socketErr.message);
287
+ }
288
+ } catch (err) {
289
+ console.error('[Niki-Loyalty] Grup bonus hatası:', err);
290
+ }
291
+ };
292
+
239
293
  // =========================
240
294
  // 🚀 INIT & ROUTES
241
295
  // =========================
@@ -249,10 +303,10 @@ Plugin.init = async function (params) {
249
303
  try {
250
304
  const uid = req.uid;
251
305
  // Heartbeat geldiğinde "read" aksiyonunu tetikle
252
- await awardDailyAction(uid, 'read');
306
+ const result = await awardDailyAction(uid, 'read');
253
307
 
254
308
  const newBalance = await user.getUserField(uid, 'niki_points');
255
- return res.json({ earned: true, total: newBalance });
309
+ return res.json({ earned: result.success, total: newBalance });
256
310
  } catch (err) {
257
311
  return res.status(500).json({ error: 'error' });
258
312
  }
@@ -284,21 +338,25 @@ Plugin.init = async function (params) {
284
338
  }
285
339
  });
286
340
 
287
- // 2) WALLET DATA (Cüzdan Bilgileri)
288
- // 2) WALLET DATA (Sayaçlar Eklendi)
341
+ // 2) WALLET DATA (Cüzdan Bilgileri + Sayaçlar)
289
342
  router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
290
343
  try {
291
344
  const uid = req.uid;
292
345
  const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
293
346
 
294
347
  // Veritabanından verileri çek
295
- const [userData, dailyData, actionCounts, historyRaw] = await Promise.all([
348
+ const [userData, dailyData, actionCounts, historyRaw, memberChecks] = await Promise.all([
296
349
  user.getUserFields(uid, ['niki_points']),
297
350
  db.getObject(`niki:daily:${uid}:${today}`),
298
- db.getObject(`niki:daily:${uid}:${today}:counts`), // <--- YENİ: Sayaçları çekiyoruz
351
+ db.getObject(`niki:daily:${uid}:${today}:counts`),
299
352
  db.getListRange(`niki:activity:${uid}`, 0, -1),
353
+ Promise.all(WALLET_GROUPS.map(g => groups.isMember(uid, g))),
300
354
  ]);
301
355
 
356
+ // Kullanıcı WALLET_GROUPS'dan herhangi birinde mi?
357
+ const canRedeem = memberChecks.some(Boolean);
358
+ const userGroup = canRedeem ? WALLET_GROUPS[memberChecks.indexOf(true)] : null;
359
+
302
360
  const dailyScore = parseFloat(dailyData?.score || 0);
303
361
  let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
304
362
  if (dailyPercent > 100) dailyPercent = 100;
@@ -310,9 +368,13 @@ Plugin.init = async function (params) {
310
368
  dailyScore,
311
369
  dailyCap: SETTINGS.dailyCap,
312
370
  dailyPercent,
313
- counts: actionCounts || {}, // <--- YENİ: Frontend'e gönderiyoruz
371
+ counts: actionCounts || {},
372
+ actions: ACTIONS,
314
373
  history,
315
374
  rewards: REWARDS,
375
+ canRedeem,
376
+ userGroup,
377
+ walletGroups: WALLET_GROUPS,
316
378
  });
317
379
  } catch (err) {
318
380
  return res.status(500).json({ points: 0, history: [] });
@@ -432,42 +494,67 @@ Plugin.init = async function (params) {
432
494
  router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
433
495
  try {
434
496
  const uid = req.uid;
497
+
498
+ // Grup kontrolü
499
+ const memberChecks = await Promise.all(WALLET_GROUPS.map(g => groups.isMember(uid, g)));
500
+ if (!memberChecks.some(Boolean)) {
501
+ return res.json({ success: false, message: 'Ödül kullanmak için Premium, Lite veya VIP grubuna katılmalısın.' });
502
+ }
503
+
504
+ const rewardIndex = parseInt(req.body.rewardIndex, 10);
435
505
  const points = parseFloat((await user.getUserField(uid, 'niki_points')) || 0);
436
- const minCost = REWARDS[REWARDS.length - 1].cost; // En ucuz ödül
437
506
 
438
- if (!TEST_MODE_UNLIMITED && points < minCost) {
439
- return res.json({ success: false, message: `Yetersiz Puan. En az ${minCost} gerekli.` });
507
+ // Seçilen ödülü bul
508
+ if (isNaN(rewardIndex) || rewardIndex < 0 || rewardIndex >= REWARDS.length) {
509
+ return res.json({ success: false, message: 'Geçersiz ödül seçimi.' });
510
+ }
511
+ const selectedReward = REWARDS[rewardIndex];
512
+
513
+ if (!TEST_MODE_UNLIMITED && points < selectedReward.cost) {
514
+ return res.json({ success: false, message: `Yetersiz Puan. ${selectedReward.name} için ${selectedReward.cost} puan gerekli.` });
440
515
  }
441
- const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
442
- await db.set(`niki:qr:${token}`, uid);
516
+ const token = crypto.randomBytes(16).toString('hex');
517
+ // Token'a kullanıcı ve seçilen ödül bilgisini kaydet
518
+ await db.setObject(`niki:qr:${token}`, { uid: String(uid), rewardIndex: String(rewardIndex) });
443
519
  await db.expire(`niki:qr:${token}`, 120); // 2 dakika geçerli
444
- return res.json({ success: true, token });
520
+ return res.json({ success: true, token, rewardName: selectedReward.name, rewardCost: selectedReward.cost });
445
521
  } catch (e) { return res.status(500).json({ success: false }); }
446
522
  });
447
523
 
448
524
  // 5) QR TARATMA (Kasa İşlemi)
449
525
  router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
450
- // ... (Mevcut kodunun aynısı)
451
526
  try {
452
527
  const token = req.body.token;
453
528
  const isAdmin = await user.isAdministrator(req.uid);
454
529
  const isMod = await user.isGlobalModerator(req.uid);
455
530
  if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
456
531
 
457
- const custUid = await db.get(`niki:qr:${token}`);
458
- if (!custUid) return res.json({ success: false, message: 'Geçersiz Kod' });
532
+ const qrData = await db.getObject(`niki:qr:${token}`);
533
+ if (!qrData || !qrData.uid) return res.json({ success: false, message: 'Geçersiz Kod' });
459
534
 
535
+ const custUid = qrData.uid;
536
+ const rewardIndex = parseInt(qrData.rewardIndex, 10);
460
537
  const pts = parseFloat(await user.getUserField(custUid, 'niki_points') || 0);
461
538
 
462
539
  let selectedReward = null;
463
540
  if (!TEST_MODE_UNLIMITED) {
464
- for (const r of REWARDS) {
465
- if (pts >= r.cost) { selectedReward = r; break; }
541
+ // Token'daki ödül index'ini kullan
542
+ if (!isNaN(rewardIndex) && rewardIndex >= 0 && rewardIndex < REWARDS.length) {
543
+ selectedReward = REWARDS[rewardIndex];
544
+ } else {
545
+ // Fallback: en yüksek ödülü seç
546
+ for (const r of REWARDS) {
547
+ if (pts >= r.cost) { selectedReward = r; break; }
548
+ }
466
549
  }
467
550
  if (!selectedReward) return res.json({ success: false, message: 'Puan Yetersiz' });
468
551
  } else { selectedReward = REWARDS[0]; }
469
552
 
470
553
  if (!TEST_MODE_UNLIMITED) {
554
+ if (pts < selectedReward.cost) {
555
+ await db.delete(`niki:qr:${token}`);
556
+ return res.json({ success: false, message: 'Puan yetersiz, işlem iptal edildi.' });
557
+ }
471
558
  await user.decrementUserFieldBy(custUid, 'niki_points', selectedReward.cost);
472
559
  }
473
560
  await db.delete(`niki:qr:${token}`);
@@ -483,7 +570,7 @@ Plugin.init = async function (params) {
483
570
  // 6) SAYFA ROTALARI
484
571
  routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
485
572
  const isStaff = await user.isAdministrator(req.uid) || await user.isGlobalModerator(req.uid);
486
- if (!isStaff) return res.render('403', {});
573
+ if (!isStaff) return routeHelpers.notAllowed(req, res);
487
574
  return res.render('niki-kasa', { title: 'Niki Kasa' });
488
575
  });
489
576
 
@@ -569,20 +656,31 @@ Plugin.socketScanQR = async function (socket, data) {
569
656
  const token = data.token;
570
657
  if (!token) throw new Error('Geçersiz Token');
571
658
 
572
- const custUid = await db.get(`niki:qr:${token}`);
573
- if (!custUid) throw new Error('QR Kod Geçersiz veya Süresi Dolmuş');
659
+ const qrData = await db.getObject(`niki:qr:${token}`);
660
+ if (!qrData || !qrData.uid) throw new Error('QR Kod Geçersiz veya Süresi Dolmuş');
574
661
 
662
+ const custUid = qrData.uid;
663
+ const rewardIndex = parseInt(qrData.rewardIndex, 10);
575
664
  const pts = parseFloat((await user.getUserField(custUid, 'niki_points')) || 0);
576
665
 
577
666
  let selectedReward = null;
578
667
  if (!TEST_MODE_UNLIMITED) {
579
- for (const r of REWARDS) {
580
- if (pts >= r.cost) { selectedReward = r; break; }
668
+ if (!isNaN(rewardIndex) && rewardIndex >= 0 && rewardIndex < REWARDS.length) {
669
+ selectedReward = REWARDS[rewardIndex];
670
+ } else {
671
+ for (const r of REWARDS) {
672
+ if (pts >= r.cost) { selectedReward = r; break; }
673
+ }
581
674
  }
582
675
  if (!selectedReward) throw new Error('Puan Yetersiz');
583
676
  } else { selectedReward = REWARDS[0]; }
584
677
 
585
678
  if (!TEST_MODE_UNLIMITED) {
679
+ const currentPts = parseFloat(await user.getUserField(custUid, 'niki_points') || 0);
680
+ if (currentPts < selectedReward.cost) {
681
+ await db.delete(`niki:qr:${token}`);
682
+ throw new Error('Puan yetersiz, işlem iptal edildi.');
683
+ }
586
684
  await user.decrementUserFieldBy(custUid, 'niki_points', selectedReward.cost);
587
685
  }
588
686
  await db.delete(`niki:qr:${token}`);
@@ -651,7 +749,12 @@ Plugin.adminManagePoints = async function (socket, data) {
651
749
  if (action === 'add') {
652
750
  await user.incrementUserFieldBy(targetUid, 'niki_points', amount);
653
751
  } else if (action === 'remove') {
654
- await user.decrementUserFieldBy(targetUid, 'niki_points', amount);
752
+ // Negatif bakiye kontrolü
753
+ const currentPts = parseFloat(await user.getUserField(targetUid, 'niki_points') || 0);
754
+ const deduction = Math.min(amount, currentPts);
755
+ if (deduction > 0) {
756
+ await user.decrementUserFieldBy(targetUid, 'niki_points', deduction);
757
+ }
655
758
  } else {
656
759
  throw new Error('Geçersiz işlem türü.');
657
760
  }
@@ -660,7 +763,9 @@ Plugin.adminManagePoints = async function (socket, data) {
660
763
  const adminUserData = await user.getUserFields(uid, ['username']);
661
764
  const logMsg = `Admin (${adminUserData.username}) tarafından ${action === 'add' ? '+' : '-'}${amount} puan. Sebep: ${reason}`;
662
765
 
663
- await addUserLog(targetUid, 'admin_adjust', amount, logMsg);
766
+ // Negatif amount ile logla, böylece frontend doğru gösterebilir
767
+ const logAmount = action === 'remove' ? -amount : amount;
768
+ await addUserLog(targetUid, 'admin_adjust', logAmount, logMsg);
664
769
 
665
770
  // Denetim Logu
666
771
  const auditLog = { ts: Date.now(), adminUid: uid, adminName: adminUserData.username, targetUid: targetUid, action: action, amount: amount, reason: reason };
@@ -700,16 +805,22 @@ Plugin.adminGetUserDetail = async function (socket, data) {
700
805
  const spendHistory = [];
701
806
 
702
807
  activities.forEach(a => {
703
- if (a.type === 'earn' || a.type === 'admin_adjust') {
704
- if (a.type === 'admin_adjust' && a.txt && a.txt.includes('-')) {
705
- totalSpent += parseFloat(a.amt) || 0;
808
+ const amt = parseFloat(a.amt) || 0;
809
+ if (a.type === 'admin_adjust') {
810
+ // amt negatifse çıkarma, pozitifse ekleme (eski kayıtlarda text'e bakarak fallback)
811
+ const isDeduction = amt < 0 || (amt > 0 && a.txt && a.txt.includes('-'));
812
+ if (isDeduction) {
813
+ totalSpent += Math.abs(amt);
706
814
  spendHistory.push(a);
707
815
  } else {
708
- totalEarned += parseFloat(a.amt) || 0;
816
+ totalEarned += amt;
709
817
  earnHistory.push(a);
710
818
  }
819
+ } else if (a.type === 'earn') {
820
+ totalEarned += amt;
821
+ earnHistory.push(a);
711
822
  } else if (a.type === 'spend') {
712
- totalSpent += parseFloat(a.amt) || 0;
823
+ totalSpent += Math.abs(amt);
713
824
  spendHistory.push(a);
714
825
  }
715
826
  });
@@ -747,7 +858,8 @@ Plugin.adminGetUserDetail = async function (socket, data) {
747
858
  totalSpent,
748
859
  currentPoints: parseFloat(userData.niki_points || 0),
749
860
  todayScore: parseFloat((dailyData && dailyData.score) || 0),
750
- todayCounts: actionCounts || {}
861
+ todayCounts: actionCounts || {},
862
+ dailyCap: SETTINGS.dailyCap
751
863
  },
752
864
  earnHistory: earnHistory.slice(0, 30),
753
865
  spendHistory: spendHistory.slice(0, 30),