nodebb-plugin-niki-loyalty 1.3.16 → 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.
- package/.claude/settings.local.json +7 -0
- package/library.js +124 -24
- package/niki-admin.txt +1309 -0
- package/niki-kasa.txt +1006 -0
- package/niki-wallet.txt +0 -0
- package/package.json +1 -1
- package/plugin.json +2 -5
- package/static/lib/client.js +72 -92
- package/static/samplefile.html +1 -1
- package/static/templates/niki-kasa.tpl +13 -0
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:
|
|
15
|
+
dailyCap: 35, // Günlük Maksimum Limit
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
// Puan Tablosu ve Limitleri (Toplam Potansiyel ~
|
|
18
|
+
// Puan Tablosu ve Limitleri (Toplam Potansiyel ~45 Puan)
|
|
18
19
|
const ACTIONS = {
|
|
19
|
-
login: { points:
|
|
20
|
-
new_topic: { points:
|
|
21
|
-
reply: { points:
|
|
22
|
-
read: { points:
|
|
23
|
-
like_given: { points: 5, limit: 2, name: 'Beğeni Atma ❤️' },
|
|
24
|
-
like_taken: { points:
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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 +=
|
|
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 +=
|
|
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),
|