nodebb-plugin-ezoic-infinite 1.8.33 → 1.8.35

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,61 +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
-
45
- let _excludeCache = new Map();
46
- const EXCLUDE_TTL = 60_000;
47
-
48
- function excludedKey(uid, excludedGroups) {
49
- return `${uid}|${(excludedGroups || []).join('|')}`;
50
- }
51
-
52
- function buildClientConfigPayload(settings, excluded) {
53
- return {
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,
67
- };
68
- }
69
-
70
- function serializeInlineConfigScript(cfg) {
71
- return `<script>window.__nbbEzoicCfg=${JSON.stringify(cfg).replace(/</g, '\\u003c')};</script>`;
72
- }
73
-
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">`;
44
+ // ── Settings cache ───────────────────────────────────────────────────────────
80
45
 
81
46
  let _settingsCache = null;
82
47
  let _settingsCacheAt = 0;
83
48
  const SETTINGS_TTL = 30_000;
84
49
 
50
+ const _excludeCache = new Map();
51
+ const EXCLUDE_TTL = 60_000;
52
+
85
53
  async function getSettings() {
86
- const now = Date.now();
87
- if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
54
+ const t = Date.now();
55
+ if (_settingsCache && (t - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
56
+
88
57
  const s = await meta.settings.get(SETTINGS_KEY);
89
58
  _settingsCacheAt = Date.now();
90
59
  _settingsCache = {
@@ -107,56 +76,97 @@ async function getSettings() {
107
76
 
108
77
  async function isUserExcluded(uid, excludedGroups) {
109
78
  if (!uid || !excludedGroups.length) return false;
110
- const key = excludedKey(uid, excludedGroups);
111
- const now = Date.now();
79
+
80
+ const key = `${uid}|${excludedGroups.join('|')}`;
81
+ const t = Date.now();
112
82
  const hit = _excludeCache.get(key);
113
- if (hit && (now - hit.at) < EXCLUDE_TTL) return hit.value;
83
+ if (hit && (t - hit.at) < EXCLUDE_TTL) return hit.value;
114
84
 
115
85
  const excludedSet = new Set(excludedGroups);
116
86
  const userGroups = await groups.getUserGroups([uid]);
117
87
  const value = (userGroups[0] || []).some(g => excludedSet.has(g.name));
118
88
 
119
- _excludeCache.set(key, { value, at: now });
89
+ _excludeCache.set(key, { value, at: Date.now() });
90
+
91
+ // Limit cache size to prevent unbounded growth
92
+ if (_excludeCache.size > 1000) {
93
+ const oldest = _excludeCache.keys().next().value;
94
+ _excludeCache.delete(oldest);
95
+ }
96
+
120
97
  return value;
121
98
  }
122
99
 
123
100
  function clearCaches() {
124
101
  _settingsCache = null;
125
102
  _settingsCacheAt = 0;
126
- _excludeCache = new Map();
103
+ _excludeCache.clear();
127
104
  }
128
105
 
129
- // ── Scripts Ezoic ──────────────────────────────────────────────────────────
106
+ // ── Client config ────────────────────────────────────────────────────────────
107
+
108
+ function buildClientConfig(settings, excluded) {
109
+ return {
110
+ excluded,
111
+ enableBetweenAds: settings.enableBetweenAds,
112
+ showFirstTopicAd: settings.showFirstTopicAd,
113
+ placeholderIds: settings.placeholderIds,
114
+ intervalPosts: settings.intervalPosts,
115
+ enableCategoryAds: settings.enableCategoryAds,
116
+ showFirstCategoryAd: settings.showFirstCategoryAd,
117
+ categoryPlaceholderIds: settings.categoryPlaceholderIds,
118
+ intervalCategories: settings.intervalCategories,
119
+ enableMessageAds: settings.enableMessageAds,
120
+ showFirstMessageAd: settings.showFirstMessageAd,
121
+ messagePlaceholderIds: settings.messagePlaceholderIds,
122
+ messageIntervalPosts: settings.messageIntervalPosts,
123
+ };
124
+ }
130
125
 
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>`;
126
+ function serializeInlineConfig(cfg) {
127
+ return `<script>window.__nbbEzoicCfg=${JSON.stringify(cfg).replace(/</g, '\\u003c')};</script>`;
128
+ }
138
129
 
139
- // ── Hooks ──────────────────────────────────────────────────────────────────
130
+ // ── Head injection ───────────────────────────────────────────────────────────
131
+
132
+ const HEAD_PRECONNECTS = [
133
+ '<link rel="preconnect" href="https://g.ezoic.net" crossorigin>',
134
+ '<link rel="preconnect" href="https://go.ezoic.net" crossorigin>',
135
+ '<link rel="preconnect" href="https://securepubads.g.doubleclick.net" crossorigin>',
136
+ '<link rel="preconnect" href="https://pagead2.googlesyndication.com" crossorigin>',
137
+ '<link rel="dns-prefetch" href="https://g.ezoic.net">',
138
+ '<link rel="dns-prefetch" href="https://securepubads.g.doubleclick.net">',
139
+ ].join('\n');
140
+
141
+ const EZOIC_SCRIPTS = [
142
+ '<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>',
143
+ '<script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>',
144
+ '<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
145
+ '<script>',
146
+ 'window.ezstandalone = window.ezstandalone || {};',
147
+ 'ezstandalone.cmd = ezstandalone.cmd || [];',
148
+ '</script>',
149
+ ].join('\n');
150
+
151
+ // ── Hooks ────────────────────────────────────────────────────────────────────
140
152
 
141
153
  plugin.onSettingsSet = function (data) {
142
- if (data && data.hash === SETTINGS_KEY) clearCaches();
154
+ if (data?.hash === SETTINGS_KEY) clearCaches();
143
155
  };
144
156
 
145
157
  plugin.addAdminNavigation = async (header) => {
146
158
  header.plugins = header.plugins || [];
147
- 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
+ });
148
164
  return header;
149
165
  };
150
166
 
151
167
  /**
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.
168
+ * Inject Ezoic scripts into <head> via templateData.customHTML.
169
+ * NodeBB v4/Harmony: header.tpl has {{customHTML}} in <head>.
160
170
  */
161
171
  plugin.injectEzoicHead = async (data) => {
162
172
  try {
@@ -164,11 +174,17 @@ plugin.injectEzoicHead = async (data) => {
164
174
  const uid = data.req?.uid ?? 0;
165
175
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
166
176
  if (!excluded) {
167
- const cfgPayload = buildClientConfigPayload(settings, excluded);
177
+ const cfg = buildClientConfig(settings, false);
168
178
  data.templateData.customHTML =
169
- HEAD_PRECONNECTS + EZOIC_SCRIPTS + serializeInlineConfigScript(cfgPayload) + (data.templateData.customHTML || '');
179
+ HEAD_PRECONNECTS + '\n' +
180
+ EZOIC_SCRIPTS + '\n' +
181
+ serializeInlineConfig(cfg) +
182
+ (data.templateData.customHTML || '');
170
183
  }
171
- } catch (_) {}
184
+ } catch (err) {
185
+ // Log but don't break rendering
186
+ console.error('[ezoic-infinite] injectEzoicHead error:', err.message);
187
+ }
172
188
  return data;
173
189
  };
174
190
 
@@ -179,7 +195,8 @@ plugin.init = async ({ router, middleware }) => {
179
195
  res.render('admin/plugins/ezoic-infinite', {
180
196
  title: 'Ezoic Infinite Ads',
181
197
  ...settings,
182
- enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
198
+ enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
199
+ enableCategoryAds_checked: settings.enableCategoryAds ? 'checked' : '',
183
200
  enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
184
201
  allGroups,
185
202
  });
@@ -189,9 +206,14 @@ plugin.init = async ({ router, middleware }) => {
189
206
  router.get('/api/admin/plugins/ezoic-infinite', render);
190
207
 
191
208
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
192
- const settings = await getSettings();
193
- const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
194
- res.json(buildClientConfigPayload(settings, excluded));
209
+ try {
210
+ const settings = await getSettings();
211
+ const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
212
+ res.json(buildClientConfig(settings, excluded));
213
+ } catch (err) {
214
+ console.error('[ezoic-infinite] config API error:', err.message);
215
+ res.status(500).json({ error: 'internal' });
216
+ }
195
217
  });
196
218
  };
197
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.33",
3
+ "version": "1.8.35",
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
+ }