nodebb-plugin-ezoic-infinite 1.7.16 → 1.7.17

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
@@ -9,8 +9,10 @@ const plugin = {};
9
9
 
10
10
  function normalizeExcludedGroups(value) {
11
11
  if (!value) return [];
12
- if (Array.isArray(value)) return value;
13
- return String(value).split(',').map(s => s.trim()).filter(Boolean);
12
+ const arr = Array.isArray(value) ? value : String(value).split(',');
13
+ return arr
14
+ .map(s => String(s).trim())
15
+ .filter(Boolean);
14
16
  }
15
17
 
16
18
  function parseBool(v, def = false) {
@@ -20,18 +22,30 @@ function parseBool(v, def = false) {
20
22
  return s === '1' || s === 'true' || s === 'on' || s === 'yes';
21
23
  }
22
24
 
25
+ let _groupsCache = null;
26
+ let _groupsCacheAt = 0;
27
+ const GROUPS_TTL = 60000; // 60s
28
+
23
29
  async function getAllGroups() {
30
+ const now = Date.now();
31
+ if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
32
+
24
33
  let names = await db.getSortedSetRange('groups:createtime', 0, -1);
25
34
  if (!names || !names.length) {
26
35
  names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
27
36
  }
28
37
  const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
29
38
  const data = await groups.getGroupsData(filtered);
39
+
30
40
  // Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
31
- const valid = data.filter(g => g && g.name);
41
+ const valid = (data || []).filter(g => g && g.name);
32
42
  valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
33
- return valid;
43
+
44
+ _groupsCache = valid;
45
+ _groupsCacheAt = now;
46
+ return _groupsCache;
34
47
  }
48
+
35
49
  let _settingsCache = null;
36
50
  let _settingsCacheAt = 0;
37
51
  const SETTINGS_TTL = 30000; // 30s
@@ -39,8 +53,13 @@ const SETTINGS_TTL = 30000; // 30s
39
53
  async function getSettings() {
40
54
  const now = Date.now();
41
55
  if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
42
- const s = await meta.settings.get(SETTINGS_KEY);
43
- _settingsCacheAt = Date.now();
56
+ let s = {};
57
+ try {
58
+ s = await meta.settings.get(SETTINGS_KEY);
59
+ } catch (err) {
60
+ s = {};
61
+ }
62
+ _settingsCacheAt = now;
44
63
  _settingsCache = {
45
64
  // Between-post ads (simple blocks) in category topic list
46
65
  enableBetweenAds: parseBool(s.enableBetweenAds, true),
@@ -60,6 +79,9 @@ async function getSettings() {
60
79
  messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
61
80
  messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
62
81
 
82
+ // Avoid globally muting console unless explicitly enabled
83
+ muteEzoicConsole: parseBool(s.muteEzoicConsole, false),
84
+
63
85
  excludedGroups: normalizeExcludedGroups(s.excludedGroups),
64
86
  };
65
87
  return _settingsCache;
@@ -68,7 +90,8 @@ async function getSettings() {
68
90
  async function isUserExcluded(uid, excludedGroups) {
69
91
  if (!uid || !excludedGroups.length) return false;
70
92
  const userGroups = await groups.getUserGroups([uid]);
71
- return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
93
+ const excluded = new Set(excludedGroups.map(n => String(n).toLowerCase().trim()).filter(Boolean));
94
+ return (userGroups[0] || []).some(g => excluded.has(String(g.name).toLowerCase().trim()));
72
95
  }
73
96
 
74
97
  plugin.onSettingsSet = function (data) {
@@ -93,21 +116,53 @@ plugin.init = async ({ router, middleware }) => {
93
116
  const settings = await getSettings();
94
117
  const allGroups = await getAllGroups();
95
118
 
119
+ const excludedSet = new Set((settings.excludedGroups || []).map(n => String(n).toLowerCase().trim()).filter(Boolean));
120
+ const allGroupsWithSelected = (allGroups || []).map(g => ({
121
+ ...g,
122
+ selected: excludedSet.has(String(g.name).toLowerCase().trim()) ? 'selected' : '',
123
+ }));
124
+
96
125
  res.render('admin/plugins/ezoic-infinite', {
97
126
  title: 'Ezoic Infinite Ads',
98
127
  ...settings,
128
+
129
+ // SSR-friendly checkbox states
99
130
  enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
131
+ showFirstTopicAd_checked: settings.showFirstTopicAd ? 'checked' : '',
132
+
133
+ enableCategoryAds_checked: settings.enableCategoryAds ? 'checked' : '',
134
+ showFirstCategoryAd_checked: settings.showFirstCategoryAd ? 'checked' : '',
135
+
100
136
  enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
101
- allGroups,
137
+ showFirstMessageAd_checked: settings.showFirstMessageAd ? 'checked' : '',
138
+
139
+ muteEzoicConsole_checked: settings.muteEzoicConsole ? 'checked' : '',
140
+
141
+ allGroups: allGroupsWithSelected,
102
142
  });
103
143
  }
104
144
 
105
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
106
- router.get('/api/admin/plugins/ezoic-infinite', render);
145
+ router.get('/admin/plugins/ezoic-infinite', middleware.ensureLoggedIn, middleware.admin.checkPrivileges, middleware.admin.buildHeader, render);
146
+ router.get('/api/admin/plugins/ezoic-infinite', middleware.ensureLoggedIn, middleware.admin.checkPrivileges, render);
107
147
 
108
148
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
109
- const settings = await getSettings();
110
- const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
149
+ let settings;
150
+ try {
151
+ settings = await getSettings();
152
+ } catch (err) {
153
+ settings = null;
154
+ }
155
+
156
+ if (!settings) {
157
+ return res.json({ excluded: false });
158
+ }
159
+
160
+ let excluded = false;
161
+ try {
162
+ excluded = await isUserExcluded(req.uid, settings.excludedGroups);
163
+ } catch (err) {
164
+ excluded = false;
165
+ }
111
166
 
112
167
  res.json({
113
168
  excluded,
@@ -123,6 +178,7 @@ plugin.init = async ({ router, middleware }) => {
123
178
  showFirstMessageAd: settings.showFirstMessageAd,
124
179
  messagePlaceholderIds: settings.messagePlaceholderIds,
125
180
  messageIntervalPosts: settings.messageIntervalPosts,
181
+ muteEzoicConsole: settings.muteEzoicConsole,
126
182
  });
127
183
  });
128
184
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.16",
3
+ "version": "1.7.17",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -12,10 +12,19 @@
12
12
  "infinite-scroll"
13
13
  ],
14
14
  "engines": {
15
- "nodebb": ">=4.0.0"
15
+ "nodebb": ">=4.0.0",
16
+ "node": ">=18"
16
17
  },
17
18
  "nbbpm": {
18
19
  "compatibility": "^4.0.0"
19
20
  },
20
- "private": false
21
- }
21
+ "private": false,
22
+ "files": [
23
+ "library.js",
24
+ "plugin.json",
25
+ "public/"
26
+ ],
27
+ "scripts": {
28
+ "lint": "node -c library.js && node -c public/admin.js && node -c public/client.js"
29
+ }
30
+ }
package/public/client.js CHANGED
@@ -84,6 +84,7 @@
84
84
  pools: { topics: [], posts: [], categories: [] },
85
85
  cursors: { topics: 0, posts: 0, categories: 0 },
86
86
  mountedIds: new Set(), // IDs Ezoic actuellement dans le DOM
87
+ reuseSeq: new Map(), // id → compteur pour générer des IDs DOM uniques
87
88
  lastShow: new Map(), // id → timestamp dernier show
88
89
 
89
90
  io: null,
@@ -129,14 +130,17 @@
129
130
 
130
131
  function parseIds(raw) {
131
132
  const out = [], seen = new Set();
132
- for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
133
+ for (const v of String(raw || '').split(/[\s,]+/).map(s => s.trim()).filter(Boolean)) {
133
134
  const n = parseInt(v, 10);
134
135
  if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
135
136
  }
136
137
  return out;
137
138
  }
138
139
 
139
- const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
140
+ // La réutilisation des IDs du pool est toujours autorisée pour éviter les
141
+ // situations "pool épuisé". Les IDs HTML des placeholders sont rendus uniques
142
+ // (suffixés) afin de ne pas dupliquer d'ID dans le DOM.
143
+ const allowReuse = () => true;
140
144
 
141
145
  const isFilled = (n) =>
142
146
  !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
@@ -229,13 +233,19 @@
229
233
  const i = S.cursors[poolKey] % pool.length;
230
234
  S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
231
235
  const id = pool[i];
232
- if (!S.mountedIds.has(id)) return id;
236
+ return id;
233
237
  }
234
238
  return null;
235
239
  }
236
240
 
237
241
  // ── Wraps DOM ──────────────────────────────────────────────────────────────
238
242
 
243
+ function nextDomPlaceholderId(id) {
244
+ const cur = (S.reuseSeq.get(id) || 0) + 1;
245
+ S.reuseSeq.set(id, cur);
246
+ return `${PH_PREFIX}${id}-${cur}`;
247
+ }
248
+
239
249
  function makeWrap(id, klass, key) {
240
250
  const w = document.createElement('div');
241
251
  w.className = `${WRAP_CLASS} ${klass}`;
@@ -244,7 +254,7 @@
244
254
  w.setAttribute(A_CREATED, String(ts()));
245
255
  w.style.cssText = 'width:100%;display:block;';
246
256
  const ph = document.createElement('div');
247
- ph.id = `${PH_PREFIX}${id}`;
257
+ ph.id = nextDomPlaceholderId(id);
248
258
  ph.setAttribute('data-ezoic-id', String(id));
249
259
  w.appendChild(ph);
250
260
  return w;
@@ -253,8 +263,6 @@
253
263
  function insertAfter(el, id, klass, key) {
254
264
  if (!el?.insertAdjacentElement) return null;
255
265
  if (findWrap(key)) return null; // ancre déjà présente
256
- if (S.mountedIds.has(id)) return null; // id déjà monté
257
- if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
258
266
  const w = makeWrap(id, klass, key);
259
267
  mutate(() => el.insertAdjacentElement('afterend', w));
260
268
  S.mountedIds.add(id);
@@ -269,7 +277,7 @@
269
277
  // unobserve(null) corrompt l'état interne de l'IO (pubads lève ensuite
270
278
  // "parameter 1 is not of type Element" sur le prochain observe).
271
279
  try {
272
- const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
280
+ const ph = w.querySelector('[data-ezoic-id]') || w.querySelector(`[id^="${PH_PREFIX}"]`);
273
281
  if (ph instanceof Element) S.io?.unobserve(ph);
274
282
  } catch (_) {}
275
283
  w.remove();
@@ -592,6 +600,10 @@
592
600
 
593
601
  function cleanup() {
594
602
  blockedUntil = ts() + 1500;
603
+
604
+ // Disconnect observers to avoid doing work while ajaxify swaps content
605
+ try { if (S.domObs) { S.domObs.disconnect(); S.domObs = null; } } catch (_) {}
606
+ try { if (window.__nbbTcfObs) { window.__nbbTcfObs.disconnect(); window.__nbbTcfObs = null; } } catch (_) {}
595
607
  mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
596
608
  S.cfg = null;
597
609
  S.pools = { topics: [], posts: [], categories: [] };
@@ -629,6 +641,7 @@
629
641
  // ── Utilitaires ────────────────────────────────────────────────────────────
630
642
 
631
643
  function muteConsole() {
644
+ if (!S.cfg || !S.cfg.muteEzoicConsole) return;
632
645
  if (window.__nbbEzMuted) return;
633
646
  window.__nbbEzMuted = true;
634
647
  const MUTED = ['[EzoicAds JS]: Placeholder Id', 'Debugger iframe already exists', `with id ${PH_PREFIX}`];
@@ -731,6 +744,11 @@
731
744
  let ticking = false;
732
745
  window.addEventListener('scroll', () => {
733
746
  if (ticking) return;
747
+ // Scroll bursts only matter on pages where content streams in
748
+ const kind = getKind();
749
+ if (kind !== 'topic' && kind !== 'categoryTopics' && kind !== 'categories') return;
750
+ if (isBlocked()) return;
751
+
734
752
  ticking = true;
735
753
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
736
754
  }, { passive: true });
package/public/style.css CHANGED
@@ -71,8 +71,8 @@
71
71
  overflow: hidden !important;
72
72
  }
73
73
 
74
- /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
75
- .ezoic-ad {
76
- margin: 0 !important;
77
- padding: 0 !important;
78
- }
74
+ /* ── Note ───────────────────────────────────────────────────────────── */
75
+ /*
76
+ On évite volontairement de cibler .ezoic-ad globalement,
77
+ pour ne pas impacter d'autres emplacements/optimisations Ezoic hors plugin.
78
+ */
@@ -8,7 +8,7 @@
8
8
  <input class="form-check-input" type="checkbox" id="enableBetweenAds" name="enableBetweenAds" {enableBetweenAds_checked}>
9
9
  <label class="form-check-label" for="enableBetweenAds">Activer les pubs entre les posts</label>
10
10
  <div class="form-check mt-2">
11
- <input class="form-check-input" type="checkbox" name="showFirstTopicAd" />
11
+ <input class="form-check-input" type="checkbox" name="showFirstTopicAd" {showFirstTopicAd_checked} />
12
12
  <label class="form-check-label">Afficher une pub après le 1er sujet</label>
13
13
  </div>
14
14
  </div>
@@ -16,7 +16,7 @@
16
16
  <div class="mb-3">
17
17
  <label class="form-label" for="placeholderIds">Pool d’IDs Ezoic (entre posts)</label>
18
18
  <textarea id="placeholderIds" name="placeholderIds" class="form-control" rows="4">{placeholderIds}</textarea>
19
- <p class="form-text">Un ID par ligne (ou séparé par virgules/espaces). Le nombre d’IDs = nombre max de pubs simultanées.</p>
19
+ <p class="form-text">Un ID par ligne (ou séparé par virgules/espaces).</p>
20
20
  </div>
21
21
 
22
22
  <div class="mb-3">
@@ -33,10 +33,10 @@
33
33
  <p class="form-text">Insère des pubs entre les catégories sur la page d’accueil (liste des catégories).</p>
34
34
 
35
35
  <div class="form-check mb-3">
36
- <input class="form-check-input" type="checkbox" id="enableCategoryAds" name="enableCategoryAds">
36
+ <input class="form-check-input" type="checkbox" id="enableCategoryAds" name="enableCategoryAds" {enableCategoryAds_checked}>
37
37
  <label class="form-check-label" for="enableCategoryAds">Activer les pubs entre les catégories</label>
38
38
  <div class="form-check mt-2">
39
- <input class="form-check-input" type="checkbox" name="showFirstCategoryAd" />
39
+ <input class="form-check-input" type="checkbox" name="showFirstCategoryAd" {showFirstCategoryAd_checked} />
40
40
  <label class="form-check-label">Afficher une pub après la 1ère catégorie</label>
41
41
  </div>
42
42
  </div>
@@ -44,7 +44,7 @@
44
44
  <div class="mb-3">
45
45
  <label class="form-label" for="categoryPlaceholderIds">Pool d’IDs Ezoic (catégories)</label>
46
46
  <textarea id="categoryPlaceholderIds" name="categoryPlaceholderIds" class="form-control" rows="4">{categoryPlaceholderIds}</textarea>
47
- <p class="form-text">IDs numériques, un par ligne. Utilise un pool dédié (différent des pools topics/messages).</p>
47
+ <p class="form-text">IDs numériques, un par ligne (ou séparés par virgules/espaces). Utilise un pool dédié (différent des pools topics/messages).</p>
48
48
  </div>
49
49
 
50
50
  <div class="mb-3">
@@ -59,7 +59,7 @@
59
59
  <input class="form-check-input" type="checkbox" id="enableMessageAds" name="enableMessageAds" {enableMessageAds_checked}>
60
60
  <label class="form-check-label" for="enableMessageAds">Activer les pubs “message”</label>
61
61
  <div class="form-check mt-2">
62
- <input class="form-check-input" type="checkbox" name="showFirstMessageAd" />
62
+ <input class="form-check-input" type="checkbox" name="showFirstMessageAd" {showFirstMessageAd_checked} />
63
63
  <label class="form-check-label">Afficher une pub après le 1er message</label>
64
64
  </div>
65
65
  </div>
@@ -67,7 +67,7 @@
67
67
  <div class="mb-3">
68
68
  <label class="form-label" for="messagePlaceholderIds">Pool d’IDs Ezoic (message)</label>
69
69
  <textarea id="messagePlaceholderIds" name="messagePlaceholderIds" class="form-control" rows="4">{messagePlaceholderIds}</textarea>
70
- <p class="form-text">Pool séparé recommandé pour éviter la réutilisation d’IDs. IMPORTANT : ne réutilise pas les mêmes IDs dans les deux pools.</p>
70
+ <p class="form-text">Pool séparé recommandé pour limiter la réutilisation d’IDs entre emplacements.</p>
71
71
  </div>
72
72
 
73
73
  <div class="mb-3">
@@ -82,12 +82,22 @@
82
82
  <label class="form-label" for="excludedGroups">Groupes exclus</label>
83
83
  <select id="excludedGroups" name="excludedGroups" class="form-select" multiple>
84
84
  <!-- BEGIN allGroups -->
85
- <option value="{allGroups.name}">{allGroups.name}</option>
85
+ <option value="{allGroups.name}" {allGroups.selected}>{allGroups.name}</option>
86
86
  <!-- END allGroups -->
87
87
  </select>
88
88
  <p class="form-text">Si l’utilisateur appartient à un de ces groupes, aucune pub n’est injectée.</p>
89
89
  </div>
90
90
 
91
+
92
+ <hr/>
93
+
94
+ <h4 class="mt-3">Options avancées</h4>
95
+
96
+ <div class="form-check mb-3">
97
+ <input class="form-check-input" type="checkbox" id="muteEzoicConsole" name="muteEzoicConsole" {muteEzoicConsole_checked}>
98
+ <label class="form-check-label" for="muteEzoicConsole">Filtrer certains logs Ezoic dans la console (évite le bruit)</label>
99
+ <p class="form-text">Désactivé par défaut pour ne pas impacter les logs d’autres plugins.</p>
100
+ </div>
91
101
  <button id="save" class="btn btn-primary">Enregistrer</button>
92
102
  </form>
93
103
  </div>