nodebb-plugin-ezoic-infinite 1.8.23 → 1.8.24
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 +135 -143
- package/package.json +1 -1
- package/public/admin.js +16 -32
- package/public/client.js +672 -577
- package/public/style.css +17 -11
package/library.js
CHANGED
|
@@ -1,205 +1,197 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const meta
|
|
3
|
+
const meta = require.main.require('./src/meta');
|
|
4
4
|
const groups = require.main.require('./src/groups');
|
|
5
|
-
const db
|
|
5
|
+
const db = require.main.require('./src/database');
|
|
6
6
|
|
|
7
7
|
const SETTINGS_KEY = 'ezoic-infinite';
|
|
8
|
-
const
|
|
8
|
+
const plugin = {};
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
<script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
|
|
12
|
-
<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
|
|
13
|
-
<script>
|
|
14
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
15
|
-
ezstandalone.cmd = ezstandalone.cmd || [];
|
|
16
|
-
</script>`;
|
|
10
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
categoryPlaceholderIds: '',
|
|
26
|
-
intervalCategories: 4,
|
|
27
|
-
enableMessageAds: false,
|
|
28
|
-
showFirstMessageAd: false,
|
|
29
|
-
messagePlaceholderIds: '',
|
|
30
|
-
messageIntervalPosts: 3,
|
|
31
|
-
excludedGroups: [],
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const CONFIG_FIELDS = Object.freeze([
|
|
35
|
-
'enableBetweenAds', 'showFirstTopicAd', 'placeholderIds', 'intervalPosts',
|
|
36
|
-
'enableCategoryAds', 'showFirstCategoryAd', 'categoryPlaceholderIds', 'intervalCategories',
|
|
37
|
-
'enableMessageAds', 'showFirstMessageAd', 'messagePlaceholderIds', 'messageIntervalPosts',
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
const plugin = {
|
|
41
|
-
_settingsCache: null,
|
|
42
|
-
_settingsCacheAt: 0,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function toBool(value, fallback = false) {
|
|
46
|
-
if (value === undefined || value === null || value === '') return fallback;
|
|
47
|
-
if (typeof value === 'boolean') return value;
|
|
48
|
-
switch (String(value).trim().toLowerCase()) {
|
|
49
|
-
case '1':
|
|
50
|
-
case 'true':
|
|
51
|
-
case 'on':
|
|
52
|
-
case 'yes':
|
|
53
|
-
return true;
|
|
54
|
-
default:
|
|
55
|
-
return false;
|
|
12
|
+
function normalizeExcludedGroups(value) {
|
|
13
|
+
if (!value) return [];
|
|
14
|
+
if (Array.isArray(value)) return value;
|
|
15
|
+
// NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
|
|
16
|
+
const s = String(value).trim();
|
|
17
|
+
if (s.startsWith('[')) {
|
|
18
|
+
try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
|
|
56
19
|
}
|
|
20
|
+
// Fallback : séparation par virgule
|
|
21
|
+
return s.split(',').map(v => v.trim()).filter(Boolean);
|
|
57
22
|
}
|
|
58
23
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
function toStringTrim(value) {
|
|
65
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
24
|
+
function parseBool(v, def = false) {
|
|
25
|
+
if (v === undefined || v === null || v === '') return def;
|
|
26
|
+
if (typeof v === 'boolean') return v;
|
|
27
|
+
const s = String(v).toLowerCase();
|
|
28
|
+
return s === '1' || s === 'true' || s === 'on' || s === 'yes';
|
|
66
29
|
}
|
|
67
30
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
31
|
+
async function getAllGroups() {
|
|
32
|
+
let names = await db.getSortedSetRange('groups:createtime', 0, -1);
|
|
33
|
+
if (!names || !names.length) {
|
|
34
|
+
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
72
35
|
}
|
|
36
|
+
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
37
|
+
const data = await groups.getGroupsData(filtered);
|
|
38
|
+
const valid = data.filter(g => g && g.name);
|
|
39
|
+
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
40
|
+
return valid;
|
|
41
|
+
}
|
|
73
42
|
|
|
74
|
-
|
|
75
|
-
if (!raw) return [];
|
|
43
|
+
// ── Settings cache (30s TTL) ────────────────────────────────────────────────
|
|
76
44
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const parsed = JSON.parse(raw);
|
|
80
|
-
if (Array.isArray(parsed)) {
|
|
81
|
-
return parsed.map(String).map(v => v.trim()).filter(Boolean);
|
|
82
|
-
}
|
|
83
|
-
} catch {}
|
|
84
|
-
}
|
|
45
|
+
let _excludeCache = new Map();
|
|
46
|
+
const EXCLUDE_TTL = 60_000;
|
|
85
47
|
|
|
86
|
-
|
|
48
|
+
function excludedKey(uid, excludedGroups) {
|
|
49
|
+
return `${uid}|${(excludedGroups || []).join('|')}`;
|
|
87
50
|
}
|
|
88
51
|
|
|
89
|
-
function
|
|
52
|
+
function buildClientConfigPayload(settings, excluded) {
|
|
90
53
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
54
|
+
excluded,
|
|
55
|
+
enableBetweenAds: settings.enableBetweenAds,
|
|
56
|
+
showFirstTopicAd: settings.showFirstTopicAd,
|
|
57
|
+
placeholderIds: settings.placeholderIds,
|
|
58
|
+
intervalPosts: settings.intervalPosts,
|
|
59
|
+
enableCategoryAds: settings.enableCategoryAds,
|
|
60
|
+
showFirstCategoryAd: settings.showFirstCategoryAd,
|
|
61
|
+
categoryPlaceholderIds: settings.categoryPlaceholderIds,
|
|
62
|
+
intervalCategories: settings.intervalCategories,
|
|
63
|
+
enableMessageAds: settings.enableMessageAds,
|
|
64
|
+
showFirstMessageAd: settings.showFirstMessageAd,
|
|
65
|
+
messagePlaceholderIds: settings.messagePlaceholderIds,
|
|
66
|
+
messageIntervalPosts: settings.messageIntervalPosts,
|
|
104
67
|
};
|
|
105
68
|
}
|
|
106
69
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
111
|
-
}
|
|
70
|
+
function serializeInlineConfigScript(cfg) {
|
|
71
|
+
return `<script>window.__nbbEzoicCfg=${JSON.stringify(cfg).replace(/</g, '\\u003c')};</script>`;
|
|
72
|
+
}
|
|
112
73
|
|
|
113
|
-
|
|
114
|
-
|
|
74
|
+
const HEAD_PRECONNECTS = `<link rel="preconnect" href="https://g.ezoic.net" crossorigin>
|
|
75
|
+
<link rel="preconnect" href="https://go.ezoic.net" crossorigin>
|
|
76
|
+
<link rel="preconnect" href="https://securepubads.g.doubleclick.net" crossorigin>
|
|
77
|
+
<link rel="preconnect" href="https://pagead2.googlesyndication.com" crossorigin>
|
|
78
|
+
<link rel="dns-prefetch" href="https://g.ezoic.net">
|
|
79
|
+
<link rel="dns-prefetch" href="https://securepubads.g.doubleclick.net">`;
|
|
115
80
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
81
|
+
let _settingsCache = null;
|
|
82
|
+
let _settingsCacheAt = 0;
|
|
83
|
+
const SETTINGS_TTL = 30_000;
|
|
120
84
|
|
|
121
85
|
async function getSettings() {
|
|
122
86
|
const now = Date.now();
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
87
|
+
if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
|
|
88
|
+
const s = await meta.settings.get(SETTINGS_KEY);
|
|
89
|
+
_settingsCacheAt = Date.now();
|
|
90
|
+
_settingsCache = {
|
|
91
|
+
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
92
|
+
showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
|
|
93
|
+
placeholderIds: (s.placeholderIds || '').trim(),
|
|
94
|
+
intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
|
|
95
|
+
enableCategoryAds: parseBool(s.enableCategoryAds, false),
|
|
96
|
+
showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
|
|
97
|
+
categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
|
|
98
|
+
intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
|
|
99
|
+
enableMessageAds: parseBool(s.enableMessageAds, false),
|
|
100
|
+
showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
|
|
101
|
+
messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
|
|
102
|
+
messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
|
|
103
|
+
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
104
|
+
};
|
|
105
|
+
return _settingsCache;
|
|
133
106
|
}
|
|
134
107
|
|
|
135
108
|
async function isUserExcluded(uid, excludedGroups) {
|
|
136
|
-
if (!uid || !
|
|
109
|
+
if (!uid || !excludedGroups.length) return false;
|
|
110
|
+
const key = excludedKey(uid, excludedGroups);
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const hit = _excludeCache.get(key);
|
|
113
|
+
if (hit && (now - hit.at) < EXCLUDE_TTL) return hit.value;
|
|
137
114
|
|
|
138
|
-
const userGroups = await groups.getUserGroups([uid]);
|
|
139
|
-
const names = (userGroups && userGroups[0]) || [];
|
|
140
115
|
const excludedSet = new Set(excludedGroups);
|
|
116
|
+
const userGroups = await groups.getUserGroups([uid]);
|
|
117
|
+
const value = (userGroups[0] || []).some(g => excludedSet.has(g.name));
|
|
141
118
|
|
|
142
|
-
|
|
119
|
+
_excludeCache.set(key, { value, at: now });
|
|
120
|
+
return value;
|
|
143
121
|
}
|
|
144
122
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
123
|
+
function clearCaches() {
|
|
124
|
+
_settingsCache = null;
|
|
125
|
+
_settingsCacheAt = 0;
|
|
126
|
+
_excludeCache = new Map();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Scripts Ezoic ──────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
const EZOIC_SCRIPTS = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
|
|
132
|
+
<script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
|
|
133
|
+
<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
|
|
134
|
+
<script>
|
|
135
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
136
|
+
ezstandalone.cmd = ezstandalone.cmd || [];
|
|
137
|
+
</script>`;
|
|
138
|
+
|
|
139
|
+
// ── Hooks ──────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
plugin.onSettingsSet = function (data) {
|
|
142
|
+
if (data && data.hash === SETTINGS_KEY) clearCaches();
|
|
150
143
|
};
|
|
151
144
|
|
|
152
|
-
plugin.addAdminNavigation = async
|
|
145
|
+
plugin.addAdminNavigation = async (header) => {
|
|
153
146
|
header.plugins = header.plugins || [];
|
|
154
|
-
header.plugins.push({
|
|
155
|
-
route: '/plugins/ezoic-infinite',
|
|
156
|
-
icon: 'fa-ad',
|
|
157
|
-
name: 'Ezoic Infinite Ads',
|
|
158
|
-
});
|
|
147
|
+
header.plugins.push({ route: '/plugins/ezoic-infinite', icon: 'fa-ad', name: 'Ezoic Infinite Ads' });
|
|
159
148
|
return header;
|
|
160
149
|
};
|
|
161
150
|
|
|
162
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
|
|
153
|
+
*
|
|
154
|
+
* NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
|
|
155
|
+
* (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
|
|
156
|
+
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
|
|
157
|
+
* et est rendu via req.app.renderAsync('header', hookReturn.templateData).
|
|
158
|
+
* On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
|
|
159
|
+
* tout en préservant ce dernier.
|
|
160
|
+
*/
|
|
161
|
+
plugin.injectEzoicHead = async (data) => {
|
|
163
162
|
try {
|
|
164
163
|
const settings = await getSettings();
|
|
165
|
-
const uid
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
const uid = data.req?.uid ?? 0;
|
|
165
|
+
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
166
|
+
if (!excluded) {
|
|
167
|
+
const cfgPayload = buildClientConfigPayload(settings, excluded);
|
|
168
|
+
data.templateData.customHTML =
|
|
169
|
+
HEAD_PRECONNECTS + EZOIC_SCRIPTS + serializeInlineConfigScript(cfgPayload) + (data.templateData.customHTML || '');
|
|
168
170
|
}
|
|
169
|
-
|
|
170
|
-
const templateData = data.templateData || (data.templateData = {});
|
|
171
|
-
templateData.customHTML = `${EZOIC_SCRIPTS}${templateData.customHTML || ''}`;
|
|
172
|
-
} catch {}
|
|
173
|
-
|
|
171
|
+
} catch (_) {}
|
|
174
172
|
return data;
|
|
175
173
|
};
|
|
176
174
|
|
|
177
|
-
plugin.init = async
|
|
178
|
-
async function
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
listNonPrivilegeGroups(),
|
|
182
|
-
]);
|
|
183
|
-
|
|
175
|
+
plugin.init = async ({ router, middleware }) => {
|
|
176
|
+
async function render(req, res) {
|
|
177
|
+
const settings = await getSettings();
|
|
178
|
+
const allGroups = await getAllGroups();
|
|
184
179
|
res.render('admin/plugins/ezoic-infinite', {
|
|
185
180
|
title: 'Ezoic Infinite Ads',
|
|
186
181
|
...settings,
|
|
187
182
|
enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
|
|
188
|
-
enableMessageAds_checked:
|
|
183
|
+
enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
|
|
189
184
|
allGroups,
|
|
190
185
|
});
|
|
191
186
|
}
|
|
192
187
|
|
|
193
|
-
router.get('/admin/plugins/ezoic-infinite',
|
|
194
|
-
router.get('/api/admin/plugins/ezoic-infinite',
|
|
188
|
+
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
|
|
189
|
+
router.get('/api/admin/plugins/ezoic-infinite', render);
|
|
195
190
|
|
|
196
191
|
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
197
192
|
const settings = await getSettings();
|
|
198
193
|
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
199
|
-
|
|
200
|
-
const payload = { excluded };
|
|
201
|
-
for (const key of CONFIG_FIELDS) payload[key] = settings[key];
|
|
202
|
-
res.json(payload);
|
|
194
|
+
res.json(buildClientConfigPayload(settings, excluded));
|
|
203
195
|
});
|
|
204
196
|
};
|
|
205
197
|
|
package/package.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -1,44 +1,28 @@
|
|
|
1
1
|
/* globals ajaxify */
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
(function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const EVENT_NS = '.ezoicInfinite';
|
|
9
|
-
|
|
10
|
-
function showSaved(alerts) {
|
|
11
|
-
if (alerts && typeof alerts.success === 'function') {
|
|
12
|
-
alerts.success('[[admin/settings:saved]]');
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
if (window.app && typeof window.app.alertSuccess === 'function') {
|
|
16
|
-
window.app.alertSuccess('[[admin/settings:saved]]');
|
|
17
|
-
}
|
|
18
|
-
}
|
|
4
|
+
(function () {
|
|
5
|
+
function init() {
|
|
6
|
+
const $form = $('.ezoic-infinite-settings');
|
|
7
|
+
if (!$form.length) return;
|
|
19
8
|
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
require(['settings', 'alerts'], function (Settings, alerts) {
|
|
10
|
+
Settings.load('ezoic-infinite', $form);
|
|
22
11
|
|
|
23
|
-
|
|
24
|
-
.off(`click${EVENT_NS}`)
|
|
25
|
-
.on(`click${EVENT_NS}`, function onSave(e) {
|
|
12
|
+
$('#save').off('click.ezoicInfinite').on('click.ezoicInfinite', function (e) {
|
|
26
13
|
e.preventDefault();
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
|
|
15
|
+
Settings.save('ezoic-infinite', $form, function () {
|
|
16
|
+
if (alerts && typeof alerts.success === 'function') {
|
|
17
|
+
alerts.success('[[admin/settings:saved]]');
|
|
18
|
+
} else if (window.app && typeof window.app.alertSuccess === 'function') {
|
|
19
|
+
window.app.alertSuccess('[[admin/settings:saved]]');
|
|
20
|
+
}
|
|
29
21
|
});
|
|
30
22
|
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function boot() {
|
|
34
|
-
const $form = $(FORM_SELECTOR);
|
|
35
|
-
if (!$form.length) return;
|
|
36
|
-
|
|
37
|
-
require(['settings', 'alerts'], function onModules(Settings, alerts) {
|
|
38
|
-
bind(Settings, alerts, $form);
|
|
39
23
|
});
|
|
40
24
|
}
|
|
41
25
|
|
|
42
|
-
$(document).ready(
|
|
43
|
-
$(window).on('action:ajaxify.end',
|
|
26
|
+
$(document).ready(init);
|
|
27
|
+
$(window).on('action:ajaxify.end', init);
|
|
44
28
|
})();
|