nodebb-plugin-ezoic-infinite 1.7.17 → 1.7.18

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,10 +9,8 @@ const plugin = {};
9
9
 
10
10
  function normalizeExcludedGroups(value) {
11
11
  if (!value) return [];
12
- const arr = Array.isArray(value) ? value : String(value).split(',');
13
- return arr
14
- .map(s => String(s).trim())
15
- .filter(Boolean);
12
+ if (Array.isArray(value)) return value;
13
+ return String(value).split(',').map(s => s.trim()).filter(Boolean);
16
14
  }
17
15
 
18
16
  function parseBool(v, def = false) {
@@ -22,30 +20,18 @@ function parseBool(v, def = false) {
22
20
  return s === '1' || s === 'true' || s === 'on' || s === 'yes';
23
21
  }
24
22
 
25
- let _groupsCache = null;
26
- let _groupsCacheAt = 0;
27
- const GROUPS_TTL = 60000; // 60s
28
-
29
23
  async function getAllGroups() {
30
- const now = Date.now();
31
- if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
32
-
33
24
  let names = await db.getSortedSetRange('groups:createtime', 0, -1);
34
25
  if (!names || !names.length) {
35
26
  names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
36
27
  }
37
28
  const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
38
29
  const data = await groups.getGroupsData(filtered);
39
-
40
30
  // Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
41
- const valid = (data || []).filter(g => g && g.name);
31
+ const valid = data.filter(g => g && g.name);
42
32
  valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
43
-
44
- _groupsCache = valid;
45
- _groupsCacheAt = now;
46
- return _groupsCache;
33
+ return valid;
47
34
  }
48
-
49
35
  let _settingsCache = null;
50
36
  let _settingsCacheAt = 0;
51
37
  const SETTINGS_TTL = 30000; // 30s
@@ -53,13 +39,8 @@ const SETTINGS_TTL = 30000; // 30s
53
39
  async function getSettings() {
54
40
  const now = Date.now();
55
41
  if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
56
- let s = {};
57
- try {
58
- s = await meta.settings.get(SETTINGS_KEY);
59
- } catch (err) {
60
- s = {};
61
- }
62
- _settingsCacheAt = now;
42
+ const s = await meta.settings.get(SETTINGS_KEY);
43
+ _settingsCacheAt = Date.now();
63
44
  _settingsCache = {
64
45
  // Between-post ads (simple blocks) in category topic list
65
46
  enableBetweenAds: parseBool(s.enableBetweenAds, true),
@@ -79,9 +60,6 @@ async function getSettings() {
79
60
  messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
80
61
  messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
81
62
 
82
- // Avoid globally muting console unless explicitly enabled
83
- muteEzoicConsole: parseBool(s.muteEzoicConsole, false),
84
-
85
63
  excludedGroups: normalizeExcludedGroups(s.excludedGroups),
86
64
  };
87
65
  return _settingsCache;
@@ -90,8 +68,7 @@ async function getSettings() {
90
68
  async function isUserExcluded(uid, excludedGroups) {
91
69
  if (!uid || !excludedGroups.length) return false;
92
70
  const userGroups = await groups.getUserGroups([uid]);
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()));
71
+ return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
95
72
  }
96
73
 
97
74
  plugin.onSettingsSet = function (data) {
@@ -116,53 +93,21 @@ plugin.init = async ({ router, middleware }) => {
116
93
  const settings = await getSettings();
117
94
  const allGroups = await getAllGroups();
118
95
 
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
-
125
96
  res.render('admin/plugins/ezoic-infinite', {
126
97
  title: 'Ezoic Infinite Ads',
127
98
  ...settings,
128
-
129
- // SSR-friendly checkbox states
130
99
  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
-
136
100
  enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
137
- showFirstMessageAd_checked: settings.showFirstMessageAd ? 'checked' : '',
138
-
139
- muteEzoicConsole_checked: settings.muteEzoicConsole ? 'checked' : '',
140
-
141
- allGroups: allGroupsWithSelected,
101
+ allGroups,
142
102
  });
143
103
  }
144
104
 
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);
105
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
106
+ router.get('/api/admin/plugins/ezoic-infinite', render);
147
107
 
148
108
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
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
- }
109
+ const settings = await getSettings();
110
+ const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
166
111
 
167
112
  res.json({
168
113
  excluded,
@@ -178,7 +123,6 @@ plugin.init = async ({ router, middleware }) => {
178
123
  showFirstMessageAd: settings.showFirstMessageAd,
179
124
  messagePlaceholderIds: settings.messagePlaceholderIds,
180
125
  messageIntervalPosts: settings.messageIntervalPosts,
181
- muteEzoicConsole: settings.muteEzoicConsole,
182
126
  });
183
127
  });
184
128
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.17",
3
+ "version": "1.7.18",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -12,19 +12,10 @@
12
12
  "infinite-scroll"
13
13
  ],
14
14
  "engines": {
15
- "nodebb": ">=4.0.0",
16
- "node": ">=18"
15
+ "nodebb": ">=4.0.0"
17
16
  },
18
17
  "nbbpm": {
19
18
  "compatibility": "^4.0.0"
20
19
  },
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
- }
20
+ "private": false
21
+ }
package/public/client.js CHANGED
@@ -84,7 +84,6 @@
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
88
87
  lastShow: new Map(), // id → timestamp dernier show
89
88
 
90
89
  io: null,
@@ -130,17 +129,14 @@
130
129
 
131
130
  function parseIds(raw) {
132
131
  const out = [], seen = new Set();
133
- for (const v of String(raw || '').split(/[\s,]+/).map(s => s.trim()).filter(Boolean)) {
132
+ for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
134
133
  const n = parseInt(v, 10);
135
134
  if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
136
135
  }
137
136
  return out;
138
137
  }
139
138
 
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;
139
+ const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
144
140
 
145
141
  const isFilled = (n) =>
146
142
  !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
@@ -233,19 +229,13 @@
233
229
  const i = S.cursors[poolKey] % pool.length;
234
230
  S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
235
231
  const id = pool[i];
236
- return id;
232
+ if (!S.mountedIds.has(id)) return id;
237
233
  }
238
234
  return null;
239
235
  }
240
236
 
241
237
  // ── Wraps DOM ──────────────────────────────────────────────────────────────
242
238
 
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
-
249
239
  function makeWrap(id, klass, key) {
250
240
  const w = document.createElement('div');
251
241
  w.className = `${WRAP_CLASS} ${klass}`;
@@ -254,7 +244,7 @@
254
244
  w.setAttribute(A_CREATED, String(ts()));
255
245
  w.style.cssText = 'width:100%;display:block;';
256
246
  const ph = document.createElement('div');
257
- ph.id = nextDomPlaceholderId(id);
247
+ ph.id = `${PH_PREFIX}${id}`;
258
248
  ph.setAttribute('data-ezoic-id', String(id));
259
249
  w.appendChild(ph);
260
250
  return w;
@@ -263,6 +253,8 @@
263
253
  function insertAfter(el, id, klass, key) {
264
254
  if (!el?.insertAdjacentElement) return null;
265
255
  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;
266
258
  const w = makeWrap(id, klass, key);
267
259
  mutate(() => el.insertAdjacentElement('afterend', w));
268
260
  S.mountedIds.add(id);
@@ -277,7 +269,7 @@
277
269
  // unobserve(null) corrompt l'état interne de l'IO (pubads lève ensuite
278
270
  // "parameter 1 is not of type Element" sur le prochain observe).
279
271
  try {
280
- const ph = w.querySelector('[data-ezoic-id]') || w.querySelector(`[id^="${PH_PREFIX}"]`);
272
+ const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
281
273
  if (ph instanceof Element) S.io?.unobserve(ph);
282
274
  } catch (_) {}
283
275
  w.remove();
@@ -600,10 +592,6 @@
600
592
 
601
593
  function cleanup() {
602
594
  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 (_) {}
607
595
  mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
608
596
  S.cfg = null;
609
597
  S.pools = { topics: [], posts: [], categories: [] };
@@ -641,7 +629,6 @@
641
629
  // ── Utilitaires ────────────────────────────────────────────────────────────
642
630
 
643
631
  function muteConsole() {
644
- if (!S.cfg || !S.cfg.muteEzoicConsole) return;
645
632
  if (window.__nbbEzMuted) return;
646
633
  window.__nbbEzMuted = true;
647
634
  const MUTED = ['[EzoicAds JS]: Placeholder Id', 'Debugger iframe already exists', `with id ${PH_PREFIX}`];
@@ -744,11 +731,6 @@
744
731
  let ticking = false;
745
732
  window.addEventListener('scroll', () => {
746
733
  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
-
752
734
  ticking = true;
753
735
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
754
736
  }, { passive: true });
package/public/style.css CHANGED
@@ -71,8 +71,8 @@
71
71
  overflow: hidden !important;
72
72
  }
73
73
 
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
- */
74
+ /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
75
+ .ezoic-ad {
76
+ margin: 0 !important;
77
+ padding: 0 !important;
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" {showFirstTopicAd_checked} />
11
+ <input class="form-check-input" type="checkbox" name="showFirstTopicAd" />
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).</p>
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>
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" {enableCategoryAds_checked}>
36
+ <input class="form-check-input" type="checkbox" id="enableCategoryAds" name="enableCategoryAds">
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" {showFirstCategoryAd_checked} />
39
+ <input class="form-check-input" type="checkbox" name="showFirstCategoryAd" />
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 (ou séparés par virgules/espaces). Utilise un pool dédié (différent des pools topics/messages).</p>
47
+ <p class="form-text">IDs numériques, un par ligne. 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" {showFirstMessageAd_checked} />
62
+ <input class="form-check-input" type="checkbox" name="showFirstMessageAd" />
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 limiter la réutilisation d’IDs entre emplacements.</p>
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>
71
71
  </div>
72
72
 
73
73
  <div class="mb-3">
@@ -82,22 +82,12 @@
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.selected}>{allGroups.name}</option>
85
+ <option value="{allGroups.name}">{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>
101
91
  <button id="save" class="btn btn-primary">Enregistrer</button>
102
92
  </form>
103
93
  </div>