nodebb-plugin-niki-loyalty 1.3.15 → 1.5.0

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');
@@ -11,17 +12,23 @@ const Plugin = {};
11
12
  // ⚙️ AYARLAR & KURALLAR (GAME LOGIC)
12
13
  // =========================
13
14
  const SETTINGS = {
14
- dailyCap: 70, // Günlük Maksimum Limit (Ne kadar kazanırsa kazansın buradan fazla alamaz) - 2x artırıldı
15
+ dailyCap: 35, // Günlük Maksimum Limit
15
16
  };
16
17
 
17
- // Puan Tablosu ve Limitleri (Toplam Potansiyel ~90 Puan) - 2x artırıldı
18
+ // Puan Tablosu ve Limitleri (Toplam Potansiyel ~45 Puan)
18
19
  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)
20
+ login: { points: 5, limit: 1, name: 'Günlük Giriş 👋' }, // 5 Puan
21
+ new_topic: { points: 5, limit: 1, name: 'Yeni Konu 📝' }, // 5 Puan
22
+ reply: { points: 5, limit: 2, name: 'Yorum Yazma 💬' }, // 5 x 2 = 10 Puan
23
+ read: { points: 1, limit: 10, name: 'Konu Okuma 👀' }, // 1 x 10 = 10 Puan
24
+ like_given: { points: 2.5, limit: 2, name: 'Beğeni Atma ❤️' }, // 2.5 x 2 = 5 Puan
25
+ like_taken: { points: 5, limit: 2, name: 'Beğeni Alma 🌟' } // 5 x 2 = 10 Puan
26
+ };
27
+
28
+ // Grup Katılım Bonusları
29
+ const GROUP_BONUSES = {
30
+ 'Premium': 30,
31
+ 'VIP': 50,
25
32
  };
26
33
 
27
34
  // Ödüller
@@ -67,7 +74,6 @@ async function addKasaLog(staffUid, customerName, customerUid, rewardName, amoun
67
74
 
68
75
  // 🔥 MERKEZİ PUAN DAĞITIM FONKSİYONU 🔥
69
76
  // Bütün puan işlemleri buradan geçer, limitleri kontrol eder.
70
- // 🔥 MERKEZİ PUAN DAĞITIM FONKSİYONU 🔥
71
77
  async function awardDailyAction(uid, actionKey) {
72
78
  try {
73
79
  const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
@@ -108,6 +114,10 @@ async function awardDailyAction(uid, actionKey) {
108
114
  await db.incrObjectFieldBy(dailyScoreKey, 'score', pointsToGive);
109
115
  await db.incrObjectFieldBy(actionCountKey, actionKey, 1);
110
116
 
117
+ // Günlük anahtarlara TTL koy (48 saat) - eski anahtarların birikmesini önle
118
+ await db.expire(dailyScoreKey, 172800);
119
+ await db.expire(actionCountKey, 172800);
120
+
111
121
  // Logla
112
122
  await addUserLog(uid, 'earn', pointsToGive, rule.name);
113
123
 
@@ -218,7 +228,7 @@ Plugin.onUpvote = async function (data) {
218
228
  }
219
229
 
220
230
  // Like Alan Kazanır (Post sahibi - kendine beğeni atamaz):
221
- if (postOwnerUid && postOwnerUid !== voterUid) {
231
+ if (postOwnerUid && String(postOwnerUid) !== String(voterUid)) {
222
232
  const likeTakenKey = `niki:liked_taken:${postOwnerUid}:${today}`;
223
233
  const alreadyTaken = await db.isSetMember(likeTakenKey, pid.toString());
224
234
 
@@ -236,6 +246,46 @@ Plugin.onUpvote = async function (data) {
236
246
  };
237
247
 
238
248
 
249
+ // 5. GRUP KATILIM BONUSU (Premium / VIP)
250
+ Plugin.onGroupJoin = async function (data) {
251
+ try {
252
+ const groupName = data.groupName;
253
+ const uid = data.uid;
254
+ if (!groupName || !uid) return;
255
+
256
+ const bonus = GROUP_BONUSES[groupName];
257
+ if (!bonus) return; // Bu grup için bonus tanımlı değil
258
+
259
+ // Aynı gruba tekrar katılırsa çift puan vermesin
260
+ const flagKey = `niki:group_bonus:${uid}:${groupName}`;
261
+ const alreadyClaimed = await db.get(flagKey);
262
+ if (alreadyClaimed) {
263
+ console.log(`[Niki-Loyalty] Grup bonusu zaten alınmış. UID: ${uid}, Group: ${groupName}`);
264
+ return;
265
+ }
266
+
267
+ await user.incrementUserFieldBy(uid, 'niki_points', bonus);
268
+ await db.set(flagKey, '1');
269
+ await addUserLog(uid, 'earn', bonus, `${groupName} Grubu Katılım Bonusu 🎉`);
270
+
271
+ console.log(`[Niki-Loyalty] ✅ GRUP BONUSU! UID: ${uid}, Group: ${groupName}, Points: +${bonus}`);
272
+
273
+ // Socket bildirimi
274
+ try {
275
+ if (socketHelpers && socketHelpers.server && socketHelpers.server.sockets) {
276
+ socketHelpers.server.sockets.in('uid_' + uid).emit('event:niki_award', {
277
+ title: 'Grup Bonusu! 🎉',
278
+ message: `${groupName} grubuna katıldığın için <strong style="color:#ffd700">+${bonus} Puan</strong> kazandın!`,
279
+ });
280
+ }
281
+ } catch (socketErr) {
282
+ console.error('[Niki-Loyalty] Socket emit hatası:', socketErr.message);
283
+ }
284
+ } catch (err) {
285
+ console.error('[Niki-Loyalty] Grup bonus hatası:', err);
286
+ }
287
+ };
288
+
239
289
  // =========================
240
290
  // 🚀 INIT & ROUTES
241
291
  // =========================
@@ -243,16 +293,43 @@ Plugin.init = async function (params) {
243
293
  const router = params.router;
244
294
  const middleware = params.middleware;
245
295
 
296
+ // 🔄 1 SEFERLİK MİGRASYON: 180+ puanlı herkesten 50 puan düş
297
+ try {
298
+ const migrationKey = 'niki:migration:deduct50_from_180plus_v1';
299
+ const alreadyDone = await db.get(migrationKey);
300
+ if (!alreadyDone) {
301
+ console.log('[Niki-Loyalty] Migration başlıyor: 180+ puanlı kullanıcılardan 50 puan düşülecek...');
302
+ const allUids = await db.getSortedSetRange('users:joindate', 0, -1);
303
+ if (allUids && allUids.length > 0) {
304
+ const usersData = await user.getUsersFields(allUids, ['uid', 'niki_points', 'username']);
305
+ let affected = 0;
306
+ for (const u of usersData) {
307
+ const pts = parseFloat(u.niki_points || 0);
308
+ if (pts >= 180) {
309
+ await user.decrementUserFieldBy(u.uid, 'niki_points', 50);
310
+ await addUserLog(u.uid, 'admin_adjust', 50, 'Sistem düzenlemesi: -50 puan (dengeleme)');
311
+ affected++;
312
+ console.log(`[Niki-Loyalty] Migration: ${u.username} (UID:${u.uid}) ${pts} → ${pts - 50} puan`);
313
+ }
314
+ }
315
+ console.log(`[Niki-Loyalty] Migration tamamlandı. Etkilenen kullanıcı: ${affected}`);
316
+ }
317
+ await db.set(migrationKey, Date.now().toString());
318
+ }
319
+ } catch (migErr) {
320
+ console.error('[Niki-Loyalty] Migration hatası:', migErr);
321
+ }
322
+
246
323
  // 1) HEARTBEAT (Artık "Okuma" Puanı veriyor)
247
324
  // Client-side script her 30-60 saniyede bir bu adrese istek atmalıdır.
248
325
  router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
249
326
  try {
250
327
  const uid = req.uid;
251
328
  // Heartbeat geldiğinde "read" aksiyonunu tetikle
252
- await awardDailyAction(uid, 'read');
329
+ const result = await awardDailyAction(uid, 'read');
253
330
 
254
331
  const newBalance = await user.getUserField(uid, 'niki_points');
255
- return res.json({ earned: true, total: newBalance });
332
+ return res.json({ earned: result.success, total: newBalance });
256
333
  } catch (err) {
257
334
  return res.status(500).json({ error: 'error' });
258
335
  }
@@ -284,8 +361,7 @@ Plugin.init = async function (params) {
284
361
  }
285
362
  });
286
363
 
287
- // 2) WALLET DATA (Cüzdan Bilgileri)
288
- // 2) WALLET DATA (Sayaçlar Eklendi)
364
+ // 2) WALLET DATA (Cüzdan Bilgileri + Sayaçlar)
289
365
  router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
290
366
  try {
291
367
  const uid = req.uid;
@@ -438,7 +514,7 @@ Plugin.init = async function (params) {
438
514
  if (!TEST_MODE_UNLIMITED && points < minCost) {
439
515
  return res.json({ success: false, message: `Yetersiz Puan. En az ${minCost} gerekli.` });
440
516
  }
441
- const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
517
+ const token = crypto.randomBytes(16).toString('hex');
442
518
  await db.set(`niki:qr:${token}`, uid);
443
519
  await db.expire(`niki:qr:${token}`, 120); // 2 dakika geçerli
444
520
  return res.json({ success: true, token });
@@ -468,6 +544,11 @@ Plugin.init = async function (params) {
468
544
  } else { selectedReward = REWARDS[0]; }
469
545
 
470
546
  if (!TEST_MODE_UNLIMITED) {
547
+ // Negatif bakiye kontrolü - önce kontrol, sonra düş
548
+ if (pts < selectedReward.cost) {
549
+ await db.delete(`niki:qr:${token}`);
550
+ return res.json({ success: false, message: 'Puan yetersiz, işlem iptal edildi.' });
551
+ }
471
552
  await user.decrementUserFieldBy(custUid, 'niki_points', selectedReward.cost);
472
553
  }
473
554
  await db.delete(`niki:qr:${token}`);
@@ -483,7 +564,7 @@ Plugin.init = async function (params) {
483
564
  // 6) SAYFA ROTALARI
484
565
  routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
485
566
  const isStaff = await user.isAdministrator(req.uid) || await user.isGlobalModerator(req.uid);
486
- if (!isStaff) return res.render('403', {});
567
+ if (!isStaff) return routeHelpers.notAllowed(req, res);
487
568
  return res.render('niki-kasa', { title: 'Niki Kasa' });
488
569
  });
489
570
 
@@ -583,6 +664,11 @@ Plugin.socketScanQR = async function (socket, data) {
583
664
  } else { selectedReward = REWARDS[0]; }
584
665
 
585
666
  if (!TEST_MODE_UNLIMITED) {
667
+ const currentPts = parseFloat(await user.getUserField(custUid, 'niki_points') || 0);
668
+ if (currentPts < selectedReward.cost) {
669
+ await db.delete(`niki:qr:${token}`);
670
+ throw new Error('Puan yetersiz, işlem iptal edildi.');
671
+ }
586
672
  await user.decrementUserFieldBy(custUid, 'niki_points', selectedReward.cost);
587
673
  }
588
674
  await db.delete(`niki:qr:${token}`);
@@ -651,7 +737,12 @@ Plugin.adminManagePoints = async function (socket, data) {
651
737
  if (action === 'add') {
652
738
  await user.incrementUserFieldBy(targetUid, 'niki_points', amount);
653
739
  } else if (action === 'remove') {
654
- await user.decrementUserFieldBy(targetUid, 'niki_points', amount);
740
+ // Negatif bakiye kontrolü
741
+ const currentPts = parseFloat(await user.getUserField(targetUid, 'niki_points') || 0);
742
+ const deduction = Math.min(amount, currentPts);
743
+ if (deduction > 0) {
744
+ await user.decrementUserFieldBy(targetUid, 'niki_points', deduction);
745
+ }
655
746
  } else {
656
747
  throw new Error('Geçersiz işlem türü.');
657
748
  }
@@ -660,7 +751,9 @@ Plugin.adminManagePoints = async function (socket, data) {
660
751
  const adminUserData = await user.getUserFields(uid, ['username']);
661
752
  const logMsg = `Admin (${adminUserData.username}) tarafından ${action === 'add' ? '+' : '-'}${amount} puan. Sebep: ${reason}`;
662
753
 
663
- await addUserLog(targetUid, 'admin_adjust', amount, logMsg);
754
+ // Negatif amount ile logla, böylece frontend doğru gösterebilir
755
+ const logAmount = action === 'remove' ? -amount : amount;
756
+ await addUserLog(targetUid, 'admin_adjust', logAmount, logMsg);
664
757
 
665
758
  // Denetim Logu
666
759
  const auditLog = { ts: Date.now(), adminUid: uid, adminName: adminUserData.username, targetUid: targetUid, action: action, amount: amount, reason: reason };
@@ -700,16 +793,22 @@ Plugin.adminGetUserDetail = async function (socket, data) {
700
793
  const spendHistory = [];
701
794
 
702
795
  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;
796
+ const amt = parseFloat(a.amt) || 0;
797
+ if (a.type === 'admin_adjust') {
798
+ // amt negatifse çıkarma, pozitifse ekleme (eski kayıtlarda text'e bakarak fallback)
799
+ const isDeduction = amt < 0 || (amt > 0 && a.txt && a.txt.includes('-'));
800
+ if (isDeduction) {
801
+ totalSpent += Math.abs(amt);
706
802
  spendHistory.push(a);
707
803
  } else {
708
- totalEarned += parseFloat(a.amt) || 0;
804
+ totalEarned += amt;
709
805
  earnHistory.push(a);
710
806
  }
807
+ } else if (a.type === 'earn') {
808
+ totalEarned += amt;
809
+ earnHistory.push(a);
711
810
  } else if (a.type === 'spend') {
712
- totalSpent += parseFloat(a.amt) || 0;
811
+ totalSpent += Math.abs(amt);
713
812
  spendHistory.push(a);
714
813
  }
715
814
  });
@@ -747,7 +846,8 @@ Plugin.adminGetUserDetail = async function (socket, data) {
747
846
  totalSpent,
748
847
  currentPoints: parseFloat(userData.niki_points || 0),
749
848
  todayScore: parseFloat((dailyData && dailyData.score) || 0),
750
- todayCounts: actionCounts || {}
849
+ todayCounts: actionCounts || {},
850
+ dailyCap: SETTINGS.dailyCap
751
851
  },
752
852
  earnHistory: earnHistory.slice(0, 30),
753
853
  spendHistory: spendHistory.slice(0, 30),