nodebb-plugin-ezoic-infinite 1.8.70 → 1.8.71

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 CHANGED
@@ -7,17 +7,18 @@ const db = require.main.require('./src/database');
7
7
  const SETTINGS_KEY = 'ezoic-infinite';
8
8
  const plugin = {};
9
9
 
10
- // ── Helpers ────────────────────────────────────────────────────────────────
10
+ // ── Helpers ──────────────────────────────────────────────────────────────────
11
11
 
12
12
  function normalizeExcludedGroups(value) {
13
13
  if (!value) return [];
14
14
  if (Array.isArray(value)) return value;
15
- // NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
16
15
  const s = String(value).trim();
17
16
  if (s.startsWith('[')) {
18
- try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
17
+ try {
18
+ const parsed = JSON.parse(s);
19
+ if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
20
+ } catch (_) {}
19
21
  }
20
- // Fallback : séparation par virgule
21
22
  return s.split(',').map(v => v.trim()).filter(Boolean);
22
23
  }
23
24
 
@@ -30,25 +31,29 @@ function parseBool(v, def = false) {
30
31
 
31
32
  async function getAllGroups() {
32
33
  let names = await db.getSortedSetRange('groups:createtime', 0, -1);
33
- if (!names || !names.length) {
34
+ if (!names?.length) {
34
35
  names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
35
36
  }
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;
37
+ return (await groups.getGroupsData(
38
+ (names || []).filter(name => !groups.isPrivilegeGroup(name))
39
+ ))
40
+ .filter(g => g?.name)
41
+ .sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
41
42
  }
42
43
 
43
- // ── Settings cache (30s TTL) ────────────────────────────────────────────────
44
+ // ── Settings cache ───────────────────────────────────────────────────────────
44
45
 
45
46
  let _settingsCache = null;
46
47
  let _settingsCacheAt = 0;
47
48
  const SETTINGS_TTL = 30_000;
48
49
 
50
+ const _excludeCache = new Map();
51
+ const EXCLUDE_TTL = 60_000;
52
+
49
53
  async function getSettings() {
50
- const now = Date.now();
51
- if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
54
+ const t = Date.now();
55
+ if (_settingsCache && (t - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
56
+
52
57
  const s = await meta.settings.get(SETTINGS_KEY);
53
58
  _settingsCacheAt = Date.now();
54
59
  _settingsCache = {
@@ -71,52 +76,116 @@ async function getSettings() {
71
76
 
72
77
  async function isUserExcluded(uid, excludedGroups) {
73
78
  if (!uid || !excludedGroups.length) return false;
79
+
80
+ const key = `${uid}|${excludedGroups.join('|')}`;
81
+ const t = Date.now();
82
+ const hit = _excludeCache.get(key);
83
+ if (hit && (t - hit.at) < EXCLUDE_TTL) return hit.value;
84
+
85
+ const excludedSet = new Set(excludedGroups);
74
86
  const userGroups = await groups.getUserGroups([uid]);
75
- return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
87
+ const value = (userGroups[0] || []).some(g => excludedSet.has(g.name));
88
+
89
+ _excludeCache.set(key, { value, at: Date.now() });
90
+ if (_excludeCache.size > 1000) {
91
+ _excludeCache.delete(_excludeCache.keys().next().value);
92
+ }
93
+
94
+ return value;
76
95
  }
77
96
 
78
- // ── Scripts Ezoic ──────────────────────────────────────────────────────────
97
+ function clearCaches() {
98
+ _settingsCache = null;
99
+ _settingsCacheAt = 0;
100
+ _excludeCache.clear();
101
+ }
79
102
 
103
+ // ── Client config ────────────────────────────────────────────────────────────
104
+
105
+ function buildClientConfig(settings, excluded) {
106
+ return {
107
+ excluded,
108
+ enableBetweenAds: settings.enableBetweenAds,
109
+ showFirstTopicAd: settings.showFirstTopicAd,
110
+ placeholderIds: settings.placeholderIds,
111
+ intervalPosts: settings.intervalPosts,
112
+ enableCategoryAds: settings.enableCategoryAds,
113
+ showFirstCategoryAd: settings.showFirstCategoryAd,
114
+ categoryPlaceholderIds: settings.categoryPlaceholderIds,
115
+ intervalCategories: settings.intervalCategories,
116
+ enableMessageAds: settings.enableMessageAds,
117
+ showFirstMessageAd: settings.showFirstMessageAd,
118
+ messagePlaceholderIds: settings.messagePlaceholderIds,
119
+ messageIntervalPosts: settings.messageIntervalPosts,
120
+ };
121
+ }
122
+
123
+ function serializeInlineConfig(cfg) {
124
+ return `<script data-cfasync="false">window.__nbbEzoicCfg=${JSON.stringify(cfg).replace(/</g, '\\u003c')};</script>`;
125
+ }
126
+
127
+ // ── Head injection ───────────────────────────────────────────────────────────
128
+
129
+ const HEAD_PRECONNECTS = [
130
+ '<link rel="preconnect" href="https://g.ezoic.net" crossorigin>',
131
+ '<link rel="preconnect" href="https://go.ezoic.net" crossorigin>',
132
+ '<link rel="preconnect" href="https://securepubads.g.doubleclick.net" crossorigin>',
133
+ '<link rel="preconnect" href="https://pagead2.googlesyndication.com" crossorigin>',
134
+ '<link rel="dns-prefetch" href="https://g.ezoic.net">',
135
+ '<link rel="dns-prefetch" href="https://securepubads.g.doubleclick.net">',
136
+ ].join('\n');
137
+
138
+ // Exact v50 script block — DO NOT change order or attributes.
139
+ // The inline stub AFTER sa.min.js is required: client.js uses ezstandalone.cmd
140
+ // before sa.min.js may have initialized it. _ezaq is NOT stubbed — sa.min.js
141
+ // defines it itself and pre-defining it can change Ezoic's init path.
80
142
  const EZOIC_SCRIPTS = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
81
143
  <script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
82
144
  <script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
145
+ <script src="//ezoicanalytics.com/analytics.js"></script>
83
146
  <script>
84
147
  window.ezstandalone = window.ezstandalone || {};
85
148
  ezstandalone.cmd = ezstandalone.cmd || [];
86
149
  </script>`;
87
150
 
88
- // ── Hooks ──────────────────────────────────────────────────────────────────
151
+ // ── Hooks ────────────────────────────────────────────────────────────────────
89
152
 
90
153
  plugin.onSettingsSet = function (data) {
91
- if (data && data.hash === SETTINGS_KEY) _settingsCache = null;
154
+ if (data?.hash === SETTINGS_KEY) clearCaches();
92
155
  };
93
156
 
94
157
  plugin.addAdminNavigation = async (header) => {
95
158
  header.plugins = header.plugins || [];
96
- header.plugins.push({ route: '/plugins/ezoic-infinite', icon: 'fa-ad', name: 'Ezoic Infinite Ads' });
159
+ header.plugins.push({
160
+ route: '/plugins/ezoic-infinite',
161
+ icon: 'fa-ad',
162
+ name: 'Ezoic Infinite Ads',
163
+ });
97
164
  return header;
98
165
  };
99
166
 
100
- /**
101
- * Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
102
- *
103
- * NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
104
- * (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
105
- * Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
106
- * et est rendu via req.app.renderAsync('header', hookReturn.templateData).
107
- * On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
108
- * tout en préservant ce dernier.
109
- */
110
167
  plugin.injectEzoicHead = async (data) => {
111
168
  try {
112
169
  const settings = await getSettings();
113
170
  const uid = data.req?.uid ?? 0;
114
171
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
115
- if (!excluded) {
116
- // Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
117
- data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
172
+
173
+ if (excluded) {
174
+ // v50: did nothing for excluded users. Keep it simple.
175
+ } else {
176
+ const cfg = buildClientConfig(settings, false);
177
+ // v50 order: EZOIC_SCRIPTS first, then existing customHTML.
178
+ // Preconnects added after scripts (link tags don't block parsing).
179
+ // Inline config after everything (optimization: avoids API fetch).
180
+ data.templateData.customHTML =
181
+ EZOIC_SCRIPTS +
182
+ HEAD_PRECONNECTS + '\n' +
183
+ serializeInlineConfig(cfg) +
184
+ (data.templateData.customHTML || '');
118
185
  }
119
- } catch (_) {}
186
+ } catch (err) {
187
+ console.error('[ezoic-infinite] injectEzoicHead error:', err.message);
188
+ }
120
189
  return data;
121
190
  };
122
191
 
@@ -127,7 +196,8 @@ plugin.init = async ({ router, middleware }) => {
127
196
  res.render('admin/plugins/ezoic-infinite', {
128
197
  title: 'Ezoic Infinite Ads',
129
198
  ...settings,
130
- enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
199
+ enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
200
+ enableCategoryAds_checked: settings.enableCategoryAds ? 'checked' : '',
131
201
  enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
132
202
  allGroups,
133
203
  });
@@ -137,23 +207,14 @@ plugin.init = async ({ router, middleware }) => {
137
207
  router.get('/api/admin/plugins/ezoic-infinite', render);
138
208
 
139
209
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
140
- const settings = await getSettings();
141
- const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
142
- res.json({
143
- excluded,
144
- enableBetweenAds: settings.enableBetweenAds,
145
- showFirstTopicAd: settings.showFirstTopicAd,
146
- placeholderIds: settings.placeholderIds,
147
- intervalPosts: settings.intervalPosts,
148
- enableCategoryAds: settings.enableCategoryAds,
149
- showFirstCategoryAd: settings.showFirstCategoryAd,
150
- categoryPlaceholderIds: settings.categoryPlaceholderIds,
151
- intervalCategories: settings.intervalCategories,
152
- enableMessageAds: settings.enableMessageAds,
153
- showFirstMessageAd: settings.showFirstMessageAd,
154
- messagePlaceholderIds: settings.messagePlaceholderIds,
155
- messageIntervalPosts: settings.messageIntervalPosts,
156
- });
210
+ try {
211
+ const settings = await getSettings();
212
+ const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
213
+ res.json(buildClientConfig(settings, excluded));
214
+ } catch (err) {
215
+ console.error('[ezoic-infinite] config API error:', err.message);
216
+ res.status(500).json({ error: 'internal' });
217
+ }
157
218
  });
158
219
  };
159
220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.70",
3
+ "version": "1.8.71",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -18,4 +18,4 @@
18
18
  "compatibility": "^4.0.0"
19
19
  },
20
20
  "private": false
21
- }
21
+ }