nodebb-plugin-niki-loyalty 1.2.1 → 1.2.3
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/library.js +40 -359
- package/package.json +1 -1
- package/plugin.json +8 -36
package/library.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
const db = require.main.require('./src/database');
|
|
4
2
|
const user = require.main.require('./src/user');
|
|
5
3
|
const posts = require.main.require('./src/posts');
|
|
6
4
|
const routeHelpers = require.main.require('./src/controllers/helpers');
|
|
7
5
|
const nconf = require.main.require('nconf');
|
|
8
6
|
const socketHelpers = require.main.require('./src/socket.io/index');
|
|
7
|
+
const SocketPlugins = require.main.require('./src/socket.io/plugins');
|
|
9
8
|
const Plugin = {};
|
|
10
9
|
|
|
11
10
|
// =========================
|
|
@@ -17,12 +16,12 @@ const SETTINGS = {
|
|
|
17
16
|
|
|
18
17
|
// Puan Tablosu ve Limitleri
|
|
19
18
|
const ACTIONS = {
|
|
20
|
-
login: { points:
|
|
21
|
-
new_topic: { points:
|
|
22
|
-
reply: { points: 3, limit: 2, name: 'Yorum Yazma 💬' },
|
|
23
|
-
read: { points: 1, limit: 8, name: 'Konu Okuma 👀' },
|
|
24
|
-
like_given: { points: 4, limit: 2, name: 'Beğeni Atma ❤️' },
|
|
25
|
-
like_taken: { points: 5, limit: 2, name: 'Beğeni Alma 🌟' }
|
|
19
|
+
login: { points: 2, limit: 1, name: 'Günlük Giriş 👋' },
|
|
20
|
+
new_topic: { points: 7, limit: 1, name: 'Yeni Konu 📝' },
|
|
21
|
+
reply: { points: 3.5, limit: 2, name: 'Yorum Yazma 💬' },
|
|
22
|
+
read: { points: 1, limit: 8, name: 'Konu Okuma 👀' }, // Heartbeat ile çalışır
|
|
23
|
+
like_given: { points: 4, limit: 2, name: 'Beğeni Atma ❤️' }, // 4 puan x 2 = max 8
|
|
24
|
+
like_taken: { points: 5, limit: 2, name: 'Beğeni Alma 🌟' } // 5 puan x 2 = max 10
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
// Ödüller
|
|
@@ -356,51 +355,6 @@ Plugin.init = async function (params) {
|
|
|
356
355
|
if (!isStaff) return res.render('403', {});
|
|
357
356
|
return res.render('niki-kasa', { title: 'Niki Kasa' });
|
|
358
357
|
});
|
|
359
|
-
|
|
360
|
-
// 7) (niki-admin artık Custom Page ile yönetiliyor, route kaldırıldı)
|
|
361
|
-
|
|
362
|
-
// 8) ADMIN API - TÜM KULLANICILARIN PUANLARI
|
|
363
|
-
router.get('/api/niki-loyalty/admin/users', middleware.ensureLoggedIn, async (req, res) => {
|
|
364
|
-
try {
|
|
365
|
-
console.log('[Niki-Loyalty] Admin API called, uid:', req.uid);
|
|
366
|
-
|
|
367
|
-
const isAdmin = await user.isAdministrator(req.uid);
|
|
368
|
-
const isMod = await user.isGlobalModerator(req.uid);
|
|
369
|
-
|
|
370
|
-
console.log('[Niki-Loyalty] isAdmin:', isAdmin, 'isMod:', isMod);
|
|
371
|
-
|
|
372
|
-
if (!isAdmin && !isMod) {
|
|
373
|
-
console.log('[Niki-Loyalty] Access denied for uid:', req.uid);
|
|
374
|
-
return res.status(403).json({ error: 'Yetkisiz' });
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Tüm kullanıcıları al (limit 500)
|
|
378
|
-
const uids = await db.getSortedSetRange('users:joindate', 0, 499);
|
|
379
|
-
if (!uids || uids.length === 0) return res.json([]);
|
|
380
|
-
|
|
381
|
-
// Kullanıcı bilgilerini al
|
|
382
|
-
const usersData = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'niki_points', 'icon:bgColor']);
|
|
383
|
-
|
|
384
|
-
// Puanları olan kullanıcıları filtrele ve sırala
|
|
385
|
-
const result = usersData
|
|
386
|
-
.map(u => ({
|
|
387
|
-
uid: u.uid,
|
|
388
|
-
username: u.username,
|
|
389
|
-
userslug: u.userslug,
|
|
390
|
-
picture: u.picture || '',
|
|
391
|
-
iconBg: u['icon:bgColor'] || '#4b5563',
|
|
392
|
-
points: parseFloat(u.niki_points || 0)
|
|
393
|
-
}))
|
|
394
|
-
.filter(u => u.points > 0) // Sadece puanı olanlar
|
|
395
|
-
.sort((a, b) => b.points - a.points); // Yüksekten düşüğe sırala
|
|
396
|
-
|
|
397
|
-
console.log('[Niki-Loyalty] Returning', result.length, 'users');
|
|
398
|
-
return res.json(result);
|
|
399
|
-
} catch (err) {
|
|
400
|
-
console.error('[Niki-Loyalty] Admin users error:', err);
|
|
401
|
-
return res.status(500).json({ error: 'Sunucu hatası' });
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
358
|
};
|
|
405
359
|
|
|
406
360
|
Plugin.addScripts = async function (scripts) {
|
|
@@ -419,318 +373,45 @@ Plugin.addNavigation = async function (nav) {
|
|
|
419
373
|
return nav;
|
|
420
374
|
};
|
|
421
375
|
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
376
|
+
// =========================
|
|
377
|
+
// 🔌 SOCKET IO FONKSİYONLARI
|
|
378
|
+
// =========================
|
|
379
|
+
Plugin.adminGetUsers = async function (socket, data) {
|
|
380
|
+
// Yetki Kontrolü
|
|
381
|
+
const uid = socket.uid;
|
|
382
|
+
if (!uid) throw new Error('Giriş yapmalısınız.');
|
|
428
383
|
|
|
429
384
|
const isAdmin = await user.isAdministrator(uid);
|
|
430
385
|
const isMod = await user.isGlobalModerator(uid);
|
|
431
386
|
|
|
432
|
-
if (!isAdmin && !isMod)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
margin: 0 0 8px;
|
|
456
|
-
font-size: 22px;
|
|
457
|
-
font-weight: 700;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
.niki-admin-header p {
|
|
461
|
-
margin: 0;
|
|
462
|
-
opacity: 0.85;
|
|
463
|
-
font-size: 13px;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
.niki-stats-grid {
|
|
467
|
-
display: grid;
|
|
468
|
-
grid-template-columns: repeat(3, 1fr);
|
|
469
|
-
gap: 12px;
|
|
470
|
-
margin-bottom: 20px;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
.niki-stat-box {
|
|
474
|
-
background: #fff;
|
|
475
|
-
border-radius: 12px;
|
|
476
|
-
padding: 16px;
|
|
477
|
-
text-align: center;
|
|
478
|
-
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
|
|
479
|
-
border: 1px solid #eee;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
.niki-stat-num {
|
|
483
|
-
font-size: 26px;
|
|
484
|
-
font-weight: 700;
|
|
485
|
-
color: #3E2723;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
.niki-stat-lbl {
|
|
489
|
-
font-size: 11px;
|
|
490
|
-
color: #8D6E63;
|
|
491
|
-
text-transform: uppercase;
|
|
492
|
-
letter-spacing: 0.5px;
|
|
493
|
-
margin-top: 4px;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
.niki-table-box {
|
|
497
|
-
background: #fff;
|
|
498
|
-
border-radius: 16px;
|
|
499
|
-
overflow: hidden;
|
|
500
|
-
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
|
501
|
-
border: 1px solid #eee;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
.niki-table-top {
|
|
505
|
-
background: #FAFAFA;
|
|
506
|
-
padding: 12px 16px;
|
|
507
|
-
border-bottom: 1px solid #eee;
|
|
508
|
-
display: flex;
|
|
509
|
-
justify-content: space-between;
|
|
510
|
-
align-items: center;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
.niki-table-t {
|
|
514
|
-
font-weight: 700;
|
|
515
|
-
color: #3E2723;
|
|
516
|
-
font-size: 14px;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
.niki-search {
|
|
520
|
-
padding: 8px 12px;
|
|
521
|
-
border: 1px solid #ddd;
|
|
522
|
-
border-radius: 8px;
|
|
523
|
-
font-size: 13px;
|
|
524
|
-
width: 180px;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
.niki-ulist {
|
|
528
|
-
list-style: none;
|
|
529
|
-
margin: 0;
|
|
530
|
-
padding: 0;
|
|
531
|
-
max-height: 450px;
|
|
532
|
-
overflow-y: auto;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
.niki-urow {
|
|
536
|
-
display: flex;
|
|
537
|
-
align-items: center;
|
|
538
|
-
padding: 12px 16px;
|
|
539
|
-
border-bottom: 1px solid #f0f0f0;
|
|
540
|
-
transition: background 0.2s;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
.niki-urow:hover {
|
|
544
|
-
background: #FAFAFA;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
.niki-rank {
|
|
548
|
-
width: 35px;
|
|
549
|
-
font-weight: 700;
|
|
550
|
-
color: #8D6E63;
|
|
551
|
-
font-size: 13px;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
.niki-avatar {
|
|
555
|
-
width: 38px;
|
|
556
|
-
height: 38px;
|
|
557
|
-
border-radius: 50%;
|
|
558
|
-
margin-right: 12px;
|
|
559
|
-
object-fit: cover;
|
|
560
|
-
border: 2px solid #EFEBE9;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
.niki-avatar-letter {
|
|
564
|
-
width: 38px;
|
|
565
|
-
height: 38px;
|
|
566
|
-
border-radius: 50%;
|
|
567
|
-
margin-right: 12px;
|
|
568
|
-
display: flex;
|
|
569
|
-
align-items: center;
|
|
570
|
-
justify-content: center;
|
|
571
|
-
font-weight: 700;
|
|
572
|
-
color: #fff;
|
|
573
|
-
font-size: 16px;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
.niki-uinfo {
|
|
577
|
-
flex: 1;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
.niki-uname {
|
|
581
|
-
font-weight: 600;
|
|
582
|
-
color: #1a1a1a;
|
|
583
|
-
font-size: 14px;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
.niki-uname a {
|
|
587
|
-
color: inherit;
|
|
588
|
-
text-decoration: none;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
.niki-uname a:hover {
|
|
592
|
-
color: #5D4037;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
.niki-pts {
|
|
596
|
-
font-weight: 700;
|
|
597
|
-
font-size: 16px;
|
|
598
|
-
color: #3E2723;
|
|
599
|
-
background: #EFEBE9;
|
|
600
|
-
padding: 6px 14px;
|
|
601
|
-
border-radius: 16px;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
.niki-loading, .niki-empty {
|
|
605
|
-
text-align: center;
|
|
606
|
-
padding: 40px;
|
|
607
|
-
color: #888;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
@media (max-width: 480px) {
|
|
611
|
-
.niki-stats-grid {
|
|
612
|
-
grid-template-columns: 1fr;
|
|
613
|
-
}
|
|
614
|
-
.niki-search {
|
|
615
|
-
width: 140px;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
</style>
|
|
619
|
-
|
|
620
|
-
<div class="niki-admin-widget">
|
|
621
|
-
<div class="niki-admin-header">
|
|
622
|
-
<h2>🐱 Niki Puan Yönetimi</h2>
|
|
623
|
-
<p>Kullanıcıların puan durumunu takip edin</p>
|
|
624
|
-
</div>
|
|
625
|
-
|
|
626
|
-
<div class="niki-stats-grid" id="niki-widget-stats">
|
|
627
|
-
<div class="niki-stat-box">
|
|
628
|
-
<div class="niki-stat-num" id="w-stat-users">-</div>
|
|
629
|
-
<div class="niki-stat-lbl">Kullanıcı</div>
|
|
630
|
-
</div>
|
|
631
|
-
<div class="niki-stat-box">
|
|
632
|
-
<div class="niki-stat-num" id="w-stat-points">-</div>
|
|
633
|
-
<div class="niki-stat-lbl">Toplam Puan</div>
|
|
634
|
-
</div>
|
|
635
|
-
<div class="niki-stat-box">
|
|
636
|
-
<div class="niki-stat-num" id="w-stat-avg">-</div>
|
|
637
|
-
<div class="niki-stat-lbl">Ortalama</div>
|
|
638
|
-
</div>
|
|
639
|
-
</div>
|
|
640
|
-
|
|
641
|
-
<div class="niki-table-box">
|
|
642
|
-
<div class="niki-table-top">
|
|
643
|
-
<span class="niki-table-t">Kullanıcı Puanları</span>
|
|
644
|
-
<input type="text" class="niki-search" id="niki-w-search" placeholder="🔍 Ara...">
|
|
645
|
-
</div>
|
|
646
|
-
<ul class="niki-ulist" id="niki-w-list">
|
|
647
|
-
<li class="niki-loading">Yükleniyor...</li>
|
|
648
|
-
</ul>
|
|
649
|
-
</div>
|
|
650
|
-
</div>
|
|
651
|
-
|
|
652
|
-
<script>
|
|
653
|
-
(function() {
|
|
654
|
-
let widgetUsers = [];
|
|
655
|
-
|
|
656
|
-
function loadWidgetUsers() {
|
|
657
|
-
$.get('/api/niki-loyalty/admin/users', function(data) {
|
|
658
|
-
widgetUsers = data || [];
|
|
659
|
-
renderWidgetUsers(widgetUsers);
|
|
660
|
-
updateWidgetStats(widgetUsers);
|
|
661
|
-
}).fail(function() {
|
|
662
|
-
$('#niki-w-list').html('<li class="niki-empty">Veriler yüklenemedi.</li>');
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function updateWidgetStats(users) {
|
|
667
|
-
const total = users.length;
|
|
668
|
-
const pts = users.reduce((s, u) => s + u.points, 0);
|
|
669
|
-
const avg = total > 0 ? Math.round(pts / total) : 0;
|
|
670
|
-
$('#w-stat-users').text(total);
|
|
671
|
-
$('#w-stat-points').text(Math.floor(pts).toLocaleString());
|
|
672
|
-
$('#w-stat-avg').text(avg);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function renderWidgetUsers(users) {
|
|
676
|
-
const list = $('#niki-w-list');
|
|
677
|
-
list.empty();
|
|
678
|
-
if (users.length === 0) {
|
|
679
|
-
list.html('<li class="niki-empty">Henüz puanı olan kullanıcı yok.</li>');
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
const rp = (window.config && window.config.relative_path) || '';
|
|
683
|
-
users.forEach((u, i) => {
|
|
684
|
-
const url = rp + '/user/' + u.userslug;
|
|
685
|
-
let av;
|
|
686
|
-
if (u.picture) {
|
|
687
|
-
av = '<img src="' + u.picture + '" class="niki-avatar" onerror="this.style.display=\'none\'">';
|
|
688
|
-
} else {
|
|
689
|
-
const l = (u.username || '?').charAt(0).toUpperCase();
|
|
690
|
-
av = '<div class="niki-avatar-letter" style="background-color:' + u.iconBg + '">' + l + '</div>';
|
|
691
|
-
}
|
|
692
|
-
list.append(
|
|
693
|
-
'<li class="niki-urow">' +
|
|
694
|
-
'<span class="niki-rank">#' + (i + 1) + '</span>' +
|
|
695
|
-
av +
|
|
696
|
-
'<div class="niki-uinfo"><div class="niki-uname"><a href="' + url + '" target="_blank">' + u.username + '</a></div></div>' +
|
|
697
|
-
'<div class="niki-pts">' + Math.floor(u.points) + ' P</div>' +
|
|
698
|
-
'</li>'
|
|
699
|
-
);
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
$('#niki-w-search').on('input', function() {
|
|
704
|
-
const q = $(this).val().toLowerCase().trim();
|
|
705
|
-
if (!q) { renderWidgetUsers(widgetUsers); return; }
|
|
706
|
-
renderWidgetUsers(widgetUsers.filter(u => u.username.toLowerCase().includes(q)));
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
if (document.readyState === 'loading') {
|
|
710
|
-
document.addEventListener('DOMContentLoaded', loadWidgetUsers);
|
|
711
|
-
} else {
|
|
712
|
-
loadWidgetUsers();
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
$(window).on('action:ajaxify.end', function() {
|
|
716
|
-
if ($('.niki-admin-widget').length) loadWidgetUsers();
|
|
717
|
-
});
|
|
718
|
-
})();
|
|
719
|
-
</script>
|
|
720
|
-
`;
|
|
721
|
-
|
|
722
|
-
return { html };
|
|
387
|
+
if (!isAdmin && !isMod) throw new Error('Yetkisiz Erişim');
|
|
388
|
+
|
|
389
|
+
// Tüm kullanıcıları al (limit 500)
|
|
390
|
+
const uids = await db.getSortedSetRange('users:joindate', 0, 499);
|
|
391
|
+
if (!uids || uids.length === 0) return [];
|
|
392
|
+
|
|
393
|
+
// Kullanıcı bilgilerini al
|
|
394
|
+
const usersData = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'niki_points', 'icon:bgColor']);
|
|
395
|
+
|
|
396
|
+
// Puanları olan kullanıcıları filtrele ve sırala
|
|
397
|
+
const result = usersData
|
|
398
|
+
.map(u => ({
|
|
399
|
+
uid: u.uid,
|
|
400
|
+
username: u.username,
|
|
401
|
+
userslug: u.userslug,
|
|
402
|
+
picture: u.picture || '',
|
|
403
|
+
iconBg: u['icon:bgColor'] || '#4b5563',
|
|
404
|
+
points: parseFloat(u.niki_points || 0)
|
|
405
|
+
}))
|
|
406
|
+
.filter(u => u.points > 0) // Sadece puanı olanlar
|
|
407
|
+
.sort((a, b) => b.points - a.points); // Yüksekten düşüğe sırala
|
|
408
|
+
|
|
409
|
+
return result;
|
|
723
410
|
};
|
|
724
411
|
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
widget: 'niki-admin-panel',
|
|
729
|
-
name: 'Niki Admin Panel',
|
|
730
|
-
description: 'Niki puan yönetimi paneli (Sadece Admin ve Modlara görünür)',
|
|
731
|
-
content: ''
|
|
732
|
-
});
|
|
733
|
-
return widgets;
|
|
412
|
+
// Soket'e kaydet (Client: socket.emit('plugins.niki.getUsers', ...))
|
|
413
|
+
SocketPlugins.niki = {
|
|
414
|
+
getUsers: Plugin.adminGetUsers
|
|
734
415
|
};
|
|
735
416
|
|
|
736
417
|
module.exports = Plugin;
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -5,42 +5,14 @@
|
|
|
5
5
|
"url": "https://forum.ieu.app",
|
|
6
6
|
"library": "./library.js",
|
|
7
7
|
"hooks": [
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
{
|
|
17
|
-
"hook": "filter:scripts.get",
|
|
18
|
-
"method": "addScripts"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"hook": "action:user.loggedIn",
|
|
22
|
-
"method": "onLogin"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"hook": "action:topic.save",
|
|
26
|
-
"method": "onTopicCreate"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"hook": "action:post.save",
|
|
30
|
-
"method": "onPostCreate"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"hook": "action:post.upvote",
|
|
34
|
-
"method": "onUpvote"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"hook": "filter:widgets.getWidgets",
|
|
38
|
-
"method": "defineWidgets"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"hook": "filter:widget.render:niki-admin-panel",
|
|
42
|
-
"method": "renderNikiAdminWidget"
|
|
43
|
-
}
|
|
8
|
+
{ "hook": "static:app.load", "method": "init" },
|
|
9
|
+
{ "hook": "filter:navigation.available", "method": "addNavigation" },
|
|
10
|
+
{ "hook": "filter:scripts.get", "method": "addScripts" },
|
|
11
|
+
|
|
12
|
+
{ "hook": "action:user.loggedIn", "method": "onLogin" },
|
|
13
|
+
{ "hook": "action:topic.save", "method": "onTopicCreate" },
|
|
14
|
+
{ "hook": "action:post.save", "method": "onPostCreate" },
|
|
15
|
+
{ "hook": "action:post.upvote", "method": "onUpvote" }
|
|
44
16
|
],
|
|
45
17
|
"staticDirs": {
|
|
46
18
|
"static": "./static"
|