nodebb-plugin-niki-loyalty 1.0.17 → 1.0.20

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.
Files changed (2) hide show
  1. package/library.js +252 -188
  2. package/package.json +1 -1
package/library.js CHANGED
@@ -3,248 +3,312 @@
3
3
  const db = require.main.require('./src/database');
4
4
  const user = require.main.require('./src/user');
5
5
  const routeHelpers = require.main.require('./src/controllers/helpers');
6
+ const nconf = require.main.require('nconf');
6
7
 
7
8
  const Plugin = {};
8
9
 
10
+ // =========================
11
+ // AYARLAR
12
+ // =========================
9
13
  const SETTINGS = {
10
- pointsPerHeartbeat: 50000000,
11
- dailyCap: 250000000000,
12
- coffeeCost: 1
14
+ pointsPerHeartbeat: 500,
15
+ dailyCap: 2500000,
16
+ coffeeCost: 250,
13
17
  };
14
18
 
19
+ // ✅ TEST: sınırsız kullanım (puan kontrolünü kapatmak için true)
20
+ const TEST_MODE_UNLIMITED = false;
21
+
15
22
  // =========================
16
23
  // JSON SAFE HELPERS
17
24
  // =========================
18
25
  function safeParseMaybeJson(x) {
19
- if (x == null) return null;
26
+ if (x == null) return null;
20
27
 
21
- // bazı DB’lerde object dönebilir
22
- if (typeof x === 'object') return x;
28
+ // bazı DB’lerde object dönebilir
29
+ if (typeof x === 'object') return x;
23
30
 
24
- if (typeof x === 'string') {
25
- try {
26
- return JSON.parse(x);
27
- } catch (e) {
28
- // "[object Object]" gibi bozuk kayıtlar
29
- return null;
30
- }
31
+ if (typeof x === 'string') {
32
+ try {
33
+ return JSON.parse(x);
34
+ } catch (e) {
35
+ // "[object Object]" gibi bozuk kayıtları atla
36
+ return null;
31
37
  }
32
- return null;
38
+ }
39
+ return null;
33
40
  }
34
41
 
35
42
  function safeStringify(obj) {
36
- try {
37
- return JSON.stringify(obj);
38
- } catch (e) {
39
- return null;
40
- }
43
+ try {
44
+ return JSON.stringify(obj);
45
+ } catch (e) {
46
+ return null;
47
+ }
41
48
  }
42
49
 
43
- // --- LOG FONKSİYONLARI ---
50
+ function makeProfileUrl(userslug) {
51
+ const rp = nconf.get('relative_path') || '';
52
+ if (!userslug) return '';
53
+ return `${rp}/user/${userslug}`;
54
+ }
55
+
56
+ // =========================
57
+ // LOG FONKSİYONLARI
58
+ // =========================
44
59
  async function addUserLog(uid, type, amount, desc) {
45
- const logEntry = {
46
- ts: Date.now(),
47
- type: type, // 'earn' veya 'spend'
48
- amt: amount,
49
- txt: desc // "Kahve Keyfi ☕" gibi metin
50
- };
51
-
52
- const payload = safeStringify(logEntry);
53
- if (!payload) return;
54
-
55
- // Öğrenci geçmişine ekle (Son 50 işlem)
56
- await db.listAppend(`niki:activity:${uid}`, payload);
57
- await db.listTrim(`niki:activity:${uid}`, -50, -1);
60
+ const logEntry = {
61
+ ts: Date.now(),
62
+ type, // 'earn' | 'spend'
63
+ amt: amount,
64
+ txt: desc,
65
+ };
66
+
67
+ const payload = safeStringify(logEntry);
68
+ if (!payload) return;
69
+
70
+ await db.listAppend(`niki:activity:${uid}`, payload);
71
+ await db.listTrim(`niki:activity:${uid}`, -50, -1);
58
72
  }
59
73
 
60
74
  async function addKasaLog(staffUid, customerName, customerUid) {
61
- const logEntry = {
62
- ts: Date.now(),
63
- staff: staffUid,
64
- cust: customerName,
65
- cuid: customerUid,
66
- amt: SETTINGS.coffeeCost
67
- };
68
-
69
- const payload = safeStringify(logEntry);
70
- if (!payload) return;
71
-
72
- // Kasa defterine ekle (Son 100 işlem)
73
- await db.listAppend('niki:kasa:history', payload);
74
- await db.listTrim('niki:kasa:history', -100, -1);
75
+ const logEntry = {
76
+ ts: Date.now(),
77
+ staff: staffUid,
78
+ cust: customerName, // bazen eski kayıtlarda boş olabilir, endpoint tamamlayacak
79
+ cuid: customerUid,
80
+ amt: SETTINGS.coffeeCost,
81
+ };
82
+
83
+ const payload = safeStringify(logEntry);
84
+ if (!payload) return;
85
+
86
+ await db.listAppend('niki:kasa:history', payload);
87
+ await db.listTrim('niki:kasa:history', -100, -1);
75
88
  }
76
89
 
90
+ // =========================
91
+ // INIT
92
+ // =========================
77
93
  Plugin.init = async function (params) {
78
- const router = params.router;
79
- const middleware = params.middleware;
94
+ const router = params.router;
95
+ const middleware = params.middleware;
80
96
 
81
- // 1. HEARTBEAT (Puan Kazanma)
82
- router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
83
- try {
84
- const uid = req.uid;
85
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
86
- const dailyKey = `niki:daily:${uid}:${today}`;
97
+ // 1) HEARTBEAT (puan kazanma)
98
+ router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
99
+ try {
100
+ const uid = req.uid;
101
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
102
+ const dailyKey = `niki:daily:${uid}:${today}`;
87
103
 
88
- const currentDailyScore = await db.getObjectField(dailyKey, 'score') || 0;
104
+ const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
89
105
 
90
- if (parseInt(currentDailyScore) >= SETTINGS.dailyCap) {
91
- return res.json({ earned: false, reason: 'daily_cap' });
92
- }
106
+ if (currentDailyScore >= SETTINGS.dailyCap) {
107
+ return res.json({ earned: false, reason: 'daily_cap' });
108
+ }
93
109
 
94
- await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
95
- await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
110
+ await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
111
+ await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
96
112
 
97
- const newBalance = await user.getUserField(uid, 'niki_points');
98
- return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
99
- } catch (err) {
100
- return res.status(500).json({ earned: false, reason: 'server_error' });
101
- }
102
- });
113
+ const newBalance = await user.getUserField(uid, 'niki_points');
114
+ return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
115
+ } catch (err) {
116
+ return res.status(500).json({ earned: false, reason: 'server_error' });
117
+ }
118
+ });
103
119
 
104
- // 2. WALLET DATA (Cüzdan + Geçmiş)
105
- router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
106
- try {
107
- const uid = req.uid;
108
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
109
-
110
- const [userData, dailyData, historyRaw] = await Promise.all([
111
- user.getUserFields(uid, ['niki_points']),
112
- db.getObject(`niki:daily:${uid}:${today}`),
113
- db.getListRange(`niki:activity:${uid}`, 0, -1)
114
- ]);
115
-
116
- const currentPoints = parseInt(userData.niki_points) || 0;
117
- const dailyScore = parseInt(dailyData ? dailyData.score : 0) || 0;
118
- let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
119
- if (dailyPercent > 100) dailyPercent = 100;
120
-
121
- // ✅ history parse
122
- const history = (historyRaw || [])
123
- .map(safeParseMaybeJson)
124
- .filter(Boolean)
125
- .reverse(); // En yeni en üstte
126
-
127
- res.json({
128
- points: currentPoints,
129
- dailyScore: dailyScore,
130
- dailyCap: SETTINGS.dailyCap,
131
- dailyPercent: dailyPercent,
132
- history
133
- });
134
- } catch (err) {
135
- return res.status(500).json({
136
- points: 0,
137
- dailyScore: 0,
138
- dailyCap: SETTINGS.dailyCap,
139
- dailyPercent: 0,
140
- history: []
141
- });
120
+ // 2) WALLET DATA (cüzdan + geçmiş)
121
+ router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
122
+ try {
123
+ const uid = req.uid;
124
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
125
+
126
+ const [userData, dailyData, historyRaw] = await Promise.all([
127
+ user.getUserFields(uid, ['niki_points']),
128
+ db.getObject(`niki:daily:${uid}:${today}`),
129
+ db.getListRange(`niki:activity:${uid}`, 0, -1),
130
+ ]);
131
+
132
+ const currentPoints = parseInt(userData?.niki_points || 0, 10);
133
+ const dailyScore = parseInt(dailyData?.score || 0, 10);
134
+
135
+ let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
136
+ if (dailyPercent > 100) dailyPercent = 100;
137
+
138
+ // history parse güvenli
139
+ const history = (historyRaw || [])
140
+ .map(safeParseMaybeJson)
141
+ .filter(Boolean)
142
+ .reverse();
143
+
144
+ return res.json({
145
+ points: currentPoints,
146
+ dailyScore,
147
+ dailyCap: SETTINGS.dailyCap,
148
+ dailyPercent,
149
+ history,
150
+ });
151
+ } catch (err) {
152
+ return res.status(500).json({
153
+ points: 0,
154
+ dailyScore: 0,
155
+ dailyCap: SETTINGS.dailyCap,
156
+ dailyPercent: 0,
157
+ history: [],
158
+ });
159
+ }
160
+ });
161
+
162
+ // 3) KASA HISTORY (admin/mod)
163
+ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
164
+ try {
165
+ const isAdmin = await user.isAdministrator(req.uid);
166
+ const isMod = await user.isGlobalModerator(req.uid);
167
+ if (!isAdmin && !isMod) return res.status(403).json([]);
168
+
169
+ const raw = await db.getListRange('niki:kasa:history', 0, -1);
170
+
171
+ // Kayıtlar bazen JSON string, bazen bozuk olabilir → güvenli parse
172
+ const rows = (raw || [])
173
+ .map((x) => {
174
+ if (!x) return null;
175
+ if (typeof x === 'object') return x;
176
+ if (typeof x === 'string') {
177
+ try { return JSON.parse(x); } catch (e) { return null; }
142
178
  }
179
+ return null;
180
+ })
181
+ .filter(Boolean)
182
+ .reverse();
183
+
184
+ // cuid’lerden uid listesi çıkar
185
+ const uids = rows
186
+ .map(r => parseInt(r.cuid, 10))
187
+ .filter(n => Number.isFinite(n) && n > 0);
188
+
189
+ // NodeBB core user datası (profile-looks mantığı)
190
+ const users = await user.getUsersFields(uids, [
191
+ 'uid', 'username', 'userslug', 'picture', 'icon:bgColor',
192
+ ]);
193
+
194
+ const userMap = {};
195
+ (users || []).forEach(u => { userMap[u.uid] = u; });
196
+
197
+ const rp = nconf.get('relative_path') || '';
198
+
199
+ const enriched = rows.map(r => {
200
+ const uid = parseInt(r.cuid, 10);
201
+ const u = userMap[uid];
202
+ if (!u) return r;
203
+
204
+ return {
205
+ ...r,
206
+ cust: u.username || r.cust || 'Bilinmeyen',
207
+ userslug: u.userslug || r.userslug || '',
208
+ picture: u.picture || r.picture || '',
209
+ iconBg: u['icon:bgColor'] || r.iconBg || '#4b5563',
210
+ profileUrl: (u.userslug ? `${rp}/user/${u.userslug}` : ''),
211
+ };
143
212
  });
144
213
 
145
- // 3. KASA GEÇMİŞİ
146
- router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
147
- try {
148
- const isAdmin = await user.isAdministrator(req.uid);
149
- const isMod = await user.isGlobalModerator(req.uid);
150
-
151
- if (!isAdmin && !isMod) return res.status(403).json([]);
152
-
153
- const historyRaw = await db.getListRange('niki:kasa:history', 0, -1);
154
-
155
- const rows = (historyRaw || [])
156
- .map(safeParseMaybeJson)
157
- .filter(Boolean)
158
- .reverse();
159
-
160
- // Kullanıcı resimlerini de ekleyerek gönder (tek kayıt bozulsa bile endpoint patlamasın)
161
- const enrichedHistory = await Promise.all(rows.map(async (item) => {
162
- if (!item || !item.cuid) return item;
163
-
164
- try {
165
- const uData = await user.getUserFields(item.cuid, ['picture']);
166
- return { ...item, picture: uData?.picture };
167
- } catch (e) {
168
- return item;
169
- }
170
- }));
171
-
172
- res.json(enrichedHistory);
173
- } catch (err) {
174
- // ✅ asla crash yok
175
- return res.status(500).json([]);
176
- }
177
- });
214
+ return res.json(enriched);
215
+ } catch (err) {
216
+ return res.status(500).json([]);
217
+ }
218
+ });
178
219
 
179
- // 4. QR OLUŞTUR
180
- router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
181
- try {
182
- const uid = req.uid;
183
- const points = parseInt(await user.getUserField(uid, 'niki_points')) || 0;
184
220
 
185
- if (points < SETTINGS.coffeeCost) return res.json({ success: false, message: 'Yetersiz Puan' });
186
221
 
187
- const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
188
- await db.set(`niki:qr:${token}`, uid);
189
- await db.expire(`niki:qr:${token}`, 120);
222
+ // 4) QR OLUŞTUR
223
+ router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
224
+ try {
225
+ const uid = req.uid;
226
+ const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
190
227
 
191
- return res.json({ success: true, token: token });
192
- } catch (err) {
193
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
194
- }
195
- });
228
+ if (!TEST_MODE_UNLIMITED && points < SETTINGS.coffeeCost) {
229
+ return res.json({ success: false, message: 'Yetersiz Puan' });
230
+ }
196
231
 
197
- // 5. QR OKUT (Ödeme Alma + LOGLAMA)
198
- router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
199
- try {
200
- const { token } = req.body;
232
+ const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
201
233
 
202
- const isAdmin = await user.isAdministrator(req.uid);
203
- const isMod = await user.isGlobalModerator(req.uid);
204
- if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
234
+ await db.set(`niki:qr:${token}`, uid);
235
+ await db.expire(`niki:qr:${token}`, 120);
205
236
 
206
- const customerUid = await db.get(`niki:qr:${token}`);
207
- if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
237
+ return res.json({ success: true, token });
238
+ } catch (err) {
239
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
240
+ }
241
+ });
208
242
 
209
- const pts = parseInt(await user.getUserField(customerUid, 'niki_points')) || 0;
210
- if (pts < SETTINGS.coffeeCost) return res.json({ success: false, message: 'Yetersiz Bakiye' });
243
+ // 5) QR OKUT (admin/mod)
244
+ router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
245
+ try {
246
+ const token = (req.body && req.body.token) ? String(req.body.token) : '';
211
247
 
212
- // İŞLEM
213
- await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
214
- await db.delete(`niki:qr:${token}`);
248
+ const isAdmin = await user.isAdministrator(req.uid);
249
+ const isMod = await user.isGlobalModerator(req.uid);
250
+ if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
215
251
 
216
- // DETAYLI LOG KAYDI
217
- const customerData = await user.getUserFields(customerUid, ['username', 'picture']);
252
+ const customerUid = await db.get(`niki:qr:${token}`);
253
+ if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
218
254
 
219
- // Cüzdan için açıklama
220
- await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
255
+ const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
256
+ if (!TEST_MODE_UNLIMITED && pts < SETTINGS.coffeeCost) {
257
+ return res.json({ success: false, message: 'Yetersiz Bakiye' });
258
+ }
221
259
 
222
- // Kasa için kayıt
223
- await addKasaLog(req.uid, customerData.username, customerUid);
260
+ // puan düşür
261
+ if (!TEST_MODE_UNLIMITED) {
262
+ await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
263
+ }
224
264
 
225
- return res.json({ success: true, customer: customerData, message: 'Onaylandı!' });
226
- } catch (err) {
227
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
228
- }
229
- });
265
+ // token tek kullanımlık
266
+ await db.delete(`niki:qr:${token}`);
230
267
 
231
- // SAYFA ROTASI
232
- routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
233
- const isAdmin = await user.isAdministrator(req.uid);
234
- const isMod = await user.isGlobalModerator(req.uid);
235
- if (!isAdmin && !isMod) return res.render('403', {});
236
- res.render('niki-kasa', { title: 'Niki Kasa' });
237
- });
268
+ // müşteri bilgisi
269
+ const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
270
+
271
+ // user log
272
+ await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
273
+
274
+ // kasa log
275
+ await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid);
276
+
277
+ return res.json({
278
+ success: true,
279
+ customer: customerData,
280
+ message: 'Onaylandı!',
281
+ });
282
+ } catch (err) {
283
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
284
+ }
285
+ });
286
+
287
+ // 6) SAYFA ROTASI (kasa sayfası)
288
+ routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
289
+ const isAdmin = await user.isAdministrator(req.uid);
290
+ const isMod = await user.isGlobalModerator(req.uid);
291
+ if (!isAdmin && !isMod) return res.render('403', {});
292
+ return res.render('niki-kasa', { title: 'Niki Kasa' });
293
+ });
238
294
  };
239
295
 
296
+ // client.js inject
240
297
  Plugin.addScripts = async function (scripts) {
241
- scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
242
- return scripts;
298
+ scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
299
+ return scripts;
243
300
  };
244
301
 
302
+ // navigation
245
303
  Plugin.addNavigation = async function (nav) {
246
- nav.push({ route: "/niki-wallet", title: "Niki Cüzdan", enabled: true, iconClass: "fa-coffee", text: "Niki Cüzdan" });
247
- return nav;
304
+ nav.push({
305
+ route: '/niki-wallet',
306
+ title: 'Niki Cüzdan',
307
+ enabled: true,
308
+ iconClass: 'fa-coffee',
309
+ text: 'Niki Cüzdan',
310
+ });
311
+ return nav;
248
312
  };
249
313
 
250
314
  module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.17",
3
+ "version": "1.0.20",
4
4
  "description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {