nodebb-plugin-niki-loyalty 1.0.17 → 1.0.18

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 +234 -189
  2. package/package.json +1 -1
package/library.js CHANGED
@@ -3,248 +3,293 @@
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
+ }
48
+ }
49
+
50
+ function makeProfileUrl(userslug) {
51
+ const rp = nconf.get('relative_path') || '';
52
+ if (!userslug) return '';
53
+ return `${rp}/user/${userslug}`;
41
54
  }
42
55
 
43
- // --- LOG FONKSİYONLARI ---
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
- });
142
- }
143
- });
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
+ const rows = (raw || [])
172
+ .map(safeParseMaybeJson)
173
+ .filter(Boolean)
174
+ .reverse();
175
+
176
+ const enriched = await Promise.all(rows.map(async (item) => {
177
+ if (!item || !item.cuid) return item;
144
178
 
145
- // 3. KASA GEÇMİŞİ
146
- router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
147
179
  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([]);
180
+ // userslug + username + picture al
181
+ const uData = await user.getUserFields(item.cuid, ['username', 'userslug', 'picture']);
182
+ const userslug = uData?.userslug;
183
+
184
+ return {
185
+ ...item,
186
+ cust: item.cust || uData?.username || 'Bilinmeyen',
187
+ picture: item.picture || uData?.picture || '',
188
+ userslug: userslug || item.userslug || '',
189
+ profileUrl: makeProfileUrl(userslug || item.userslug),
190
+ };
191
+ } catch (e) {
192
+ // tek item patlasa bile endpoint düşmesin
193
+ return item;
176
194
  }
177
- });
195
+ }));
178
196
 
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;
197
+ return res.json(enriched);
198
+ } catch (err) {
199
+ return res.status(500).json([]);
200
+ }
201
+ });
184
202
 
185
- if (points < SETTINGS.coffeeCost) return res.json({ success: false, message: 'Yetersiz Puan' });
203
+ // 4) QR OLUŞTUR
204
+ router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
205
+ try {
206
+ const uid = req.uid;
207
+ const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
186
208
 
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);
209
+ if (!TEST_MODE_UNLIMITED && points < SETTINGS.coffeeCost) {
210
+ return res.json({ success: false, message: 'Yetersiz Puan' });
211
+ }
190
212
 
191
- return res.json({ success: true, token: token });
192
- } catch (err) {
193
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
194
- }
195
- });
213
+ const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
196
214
 
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;
215
+ await db.set(`niki:qr:${token}`, uid);
216
+ await db.expire(`niki:qr:${token}`, 120);
201
217
 
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' });
218
+ return res.json({ success: true, token });
219
+ } catch (err) {
220
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
221
+ }
222
+ });
205
223
 
206
- const customerUid = await db.get(`niki:qr:${token}`);
207
- if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
224
+ // 5) QR OKUT (admin/mod)
225
+ router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
226
+ try {
227
+ const token = (req.body && req.body.token) ? String(req.body.token) : '';
208
228
 
209
- const pts = parseInt(await user.getUserField(customerUid, 'niki_points')) || 0;
210
- if (pts < SETTINGS.coffeeCost) return res.json({ success: false, message: 'Yetersiz Bakiye' });
229
+ const isAdmin = await user.isAdministrator(req.uid);
230
+ const isMod = await user.isGlobalModerator(req.uid);
231
+ if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
211
232
 
212
- // İŞLEM
213
- await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
214
- await db.delete(`niki:qr:${token}`);
233
+ const customerUid = await db.get(`niki:qr:${token}`);
234
+ if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
215
235
 
216
- // DETAYLI LOG KAYDI
217
- const customerData = await user.getUserFields(customerUid, ['username', 'picture']);
236
+ const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
237
+ if (!TEST_MODE_UNLIMITED && pts < SETTINGS.coffeeCost) {
238
+ return res.json({ success: false, message: 'Yetersiz Bakiye' });
239
+ }
218
240
 
219
- // Cüzdan için açıklama
220
- await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
241
+ // puan düşür
242
+ if (!TEST_MODE_UNLIMITED) {
243
+ await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
244
+ }
221
245
 
222
- // Kasa için kayıt
223
- await addKasaLog(req.uid, customerData.username, customerUid);
246
+ // token tek kullanımlık
247
+ await db.delete(`niki:qr:${token}`);
224
248
 
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
- });
230
-
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
- });
249
+ // müşteri bilgisi
250
+ const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
251
+
252
+ // user log
253
+ await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
254
+
255
+ // kasa log
256
+ await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid);
257
+
258
+ return res.json({
259
+ success: true,
260
+ customer: customerData,
261
+ message: 'Onaylandı!',
262
+ });
263
+ } catch (err) {
264
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
265
+ }
266
+ });
267
+
268
+ // 6) SAYFA ROTASI (kasa sayfası)
269
+ routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
270
+ const isAdmin = await user.isAdministrator(req.uid);
271
+ const isMod = await user.isGlobalModerator(req.uid);
272
+ if (!isAdmin && !isMod) return res.render('403', {});
273
+ return res.render('niki-kasa', { title: 'Niki Kasa' });
274
+ });
238
275
  };
239
276
 
277
+ // client.js inject
240
278
  Plugin.addScripts = async function (scripts) {
241
- scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
242
- return scripts;
279
+ scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
280
+ return scripts;
243
281
  };
244
282
 
283
+ // navigation
245
284
  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;
285
+ nav.push({
286
+ route: '/niki-wallet',
287
+ title: 'Niki Cüzdan',
288
+ enabled: true,
289
+ iconClass: 'fa-coffee',
290
+ text: 'Niki Cüzdan',
291
+ });
292
+ return nav;
248
293
  };
249
294
 
250
295
  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.18",
4
4
  "description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {